From 2184961e3c58274e753a9f79872ec2e5a8c61750 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Sat, 10 Apr 2021 17:13:09 +0800 Subject: [PATCH] Add: download and patch JavaFX before start. Closes #800. --- .../main/java/org/jackhuang/hmcl/Main.java | 25 +- .../jackhuang/hmcl/upgrade/UpdateHandler.java | 2 +- .../hmcl/util/SelfDependencyPatcher.java | 77 +---- HMCLCore/src/main/java/java/lang/Module.java | 16 ++ .../src/main/java/java/lang/ModuleLayer.java | 20 ++ .../java/java/lang/module/ModuleFinder.java | 17 ++ .../java/java/lang/module/ModuleReader.java | 18 ++ .../java/lang/module/ModuleReference.java | 14 + .../java/org/jackhuang/hmcl/mod/ModInfo.java | 4 + .../org/jackhuang/hmcl/util/ClassUtils.java | 13 + .../org/jackhuang/hmcl/util/Java9Util.java | 65 +++++ .../java/org/jackhuang/hmcl/util/VMUtils.java | 270 ++++++++++++++++++ 12 files changed, 452 insertions(+), 89 deletions(-) create mode 100644 HMCLCore/src/main/java/java/lang/Module.java create mode 100644 HMCLCore/src/main/java/java/lang/ModuleLayer.java create mode 100644 HMCLCore/src/main/java/java/lang/module/ModuleFinder.java create mode 100644 HMCLCore/src/main/java/java/lang/module/ModuleReader.java create mode 100644 HMCLCore/src/main/java/java/lang/module/ModuleReference.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/ClassUtils.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/Java9Util.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/VMUtils.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java index 72a024d0f..ced378fe6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java @@ -19,14 +19,13 @@ package org.jackhuang.hmcl; import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.SelfDependencyPatcher; +import org.jackhuang.hmcl.util.VMUtils; import javax.net.ssl.*; import javax.swing.*; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -37,7 +36,6 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Collections; -import java.util.function.Consumer; import java.util.logging.Level; import static org.jackhuang.hmcl.util.Lang.thread; @@ -61,15 +59,14 @@ public final class Main { Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs")); - checkJavaFX(classLoader -> { - try { - Class c = Class.forName("org.jackhuang.hmcl.Launcher", true, classLoader); - Method method = c.getDeclaredMethod("main"); - method.invoke(null, (Object) args); - } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new InternalError(e); - } - }); + VMUtils.patch(); + + checkJavaFX(); + + // Fix title bar not displaying in GTK systems + System.setProperty("jdk.gtk.version", "2"); + + Launcher.main(args); } private static void checkDirectoryPath() { @@ -81,9 +78,9 @@ public final class Main { } } - private static void checkJavaFX(Consumer runnable) { + private static void checkJavaFX() { try { - SelfDependencyPatcher.runInJavaFxEnvironment(runnable); + SelfDependencyPatcher.patch(); } catch (SelfDependencyPatcher.PatchException e) { LOG.log(Level.SEVERE, "unable to patch JVM", e); showErrorAndExit(i18n("fatal.javafx.missing")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index ed79452e7..9486f1771 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -199,7 +199,7 @@ public final class UpdateHandler { StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace(); for (int i = 0; i < stacktrace.length; i++) { StackTraceElement element = stacktrace[i]; - if (Main.class.getName().equals(element.getClassName())) { + if (Main.class.getName().equals(element.getClassName()) && "main".equals(element.getMethodName())) { // we've reached the main method return i + 1 != stacktrace.length; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java index 2e30cddbf..8759146b1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java @@ -8,7 +8,6 @@ import javax.swing.*; import java.awt.*; import java.io.IOException; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; @@ -19,7 +18,6 @@ import java.util.List; import java.util.*; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; -import java.util.function.Consumer; import java.util.logging.Level; import static java.lang.Class.forName; @@ -89,15 +87,12 @@ public class SelfDependencyPatcher { /** * Patch in any missing dependencies, if any. */ - public static void runInJavaFxEnvironment(Consumer runnable) throws PatchException, IncompatibleVersionException { - if (CURRENT_JAVA.getParsedVersion() > 8) { - patchReflectionFilters(); - } + public static void patch() throws PatchException, IncompatibleVersionException { // Do nothing if JavaFX is detected try { try { forName("javafx.application.Application"); - runnable.accept(SelfDependencyPatcher.class.getClassLoader()); + return; } catch (Exception ignored) { } } catch (UnsupportedClassVersionError error) { @@ -140,33 +135,12 @@ public class SelfDependencyPatcher { loadFromCache(); } catch (IOException ex) { throw new PatchException("Failed to load JavaFX cache", ex); - } catch (ReflectiveOperationException ex) { + } catch (ReflectiveOperationException | NoClassDefFoundError ex) { throw new PatchException("Failed to add dependencies to classpath!", ex); } LOG.info(" - Done!"); } - -// /** -// * Inject them into the current classpath. -// * -// * @throws IOException When the locally cached dependency urls cannot be resolved. -// * @throws ReflectiveOperationException When the call to add these urls to the system classpath failed. -// */ -// private static void loadFromCache(Consumer runnable) throws IOException, ReflectiveOperationException { -// LOG.info(" - Loading dependencies..."); -// // Get Jar URLs -// List jarUrls = new ArrayList<>(); -// Files.walk(DEPENDENCIES_DIR_PATH).forEach(path -> { -// try { -// jarUrls.add(path.toUri().toURL()); -// } catch (MalformedURLException ex) { -// LOG.log(Level.WARNING, "Failed to convert '" + path.toFile().getAbsolutePath() + "' to URL", ex); -// } -// }); -// ClassLoader classLoader = new URLClassLoader(jarUrls.toArray(new URL[0]), SelfDependencyPatcher.class.getClassLoader()); -// runnable.accept(classLoader); -// } /** * Inject them into the current classpath. * @@ -312,51 +286,6 @@ public class SelfDependencyPatcher { } } - /** - * Patches reflection filters. - */ - private static void patchReflectionFilters() { - Class klass; - try { - klass = Class.forName("jdk.internal.reflect.Reflection", - true, null); - } catch (ClassNotFoundException ex) { - throw new RuntimeException("Unable to locate 'jdk.internal.reflect.Reflection' class", ex); - } - try { - Field[] fields; - try { - Method m = Class.class.getDeclaredMethod("getDeclaredFieldsImpl"); - ReflectionHelper.setAccessible(m); - fields = (Field[]) m.invoke(klass); - } catch (NoSuchMethodException | InvocationTargetException ex) { - try { - Method m = Class.class.getDeclaredMethod("getDeclaredFields0", Boolean.TYPE); - ReflectionHelper.setAccessible(m); - fields = (Field[]) m.invoke(klass, false); - } catch (InvocationTargetException | NoSuchMethodException ex1) { - ex.addSuppressed(ex1); - throw new RuntimeException("Unable to get all class fields", ex); - } - } - int c = 0; - for (Field field : fields) { - String name = field.getName(); - if ("fieldFilterMap".equals(name) || "methodFilterMap".equals(name)) { - ReflectionHelper.setAccessible(field); - field.set(null, new HashMap<>(0)); - if (++c == 2) { - return; - } - } - } - throw new RuntimeException("One of field patches did not apply properly. " + - "Expected to patch two fields, but patched: " + c); - } catch (IllegalAccessException | InvocationTargetException ex) { - throw new RuntimeException("Unable to patch reflection filters", ex); - } - } - public static class PatchException extends Exception { PatchException(String message, Throwable cause) { super(message, cause); diff --git a/HMCLCore/src/main/java/java/lang/Module.java b/HMCLCore/src/main/java/java/lang/Module.java new file mode 100644 index 000000000..c4180f357 --- /dev/null +++ b/HMCLCore/src/main/java/java/lang/Module.java @@ -0,0 +1,16 @@ +package java.lang; + +import java.util.Set; + +/** + * Dummy java compatibility class + * + * @author Matt + */ +public abstract class Module { + + //CHECKSTYLE:OFF + public ModuleLayer getLayer() { throw new UnsupportedOperationException(); } + public Set getPackages() { throw new UnsupportedOperationException(); } + //CHECKSTYLE:ON +} diff --git a/HMCLCore/src/main/java/java/lang/ModuleLayer.java b/HMCLCore/src/main/java/java/lang/ModuleLayer.java new file mode 100644 index 000000000..c4e418033 --- /dev/null +++ b/HMCLCore/src/main/java/java/lang/ModuleLayer.java @@ -0,0 +1,20 @@ +package java.lang; + +import java.util.Set; + +/** + * Dummy java compatibility class + * + * @author xxDark + */ +public abstract class ModuleLayer { + + //CHECKSTYLE:OFF + public Set modules() { + throw new UnsupportedOperationException(); + } + public static ModuleLayer boot() { + throw new UnsupportedOperationException(); + } + //CHECKSTYLE:ON +} diff --git a/HMCLCore/src/main/java/java/lang/module/ModuleFinder.java b/HMCLCore/src/main/java/java/lang/module/ModuleFinder.java new file mode 100644 index 000000000..f5a52745a --- /dev/null +++ b/HMCLCore/src/main/java/java/lang/module/ModuleFinder.java @@ -0,0 +1,17 @@ +package java.lang.module; + +import java.util.Set; + +/** + * Dummy java compatibility class + * + * @author xxDark + */ +public interface ModuleFinder { + //CHECKSTYLE:OFF + static ModuleFinder ofSystem() { + throw new UnsupportedOperationException(); + } + Set findAll(); + //CHECKSTYLE:ON +} diff --git a/HMCLCore/src/main/java/java/lang/module/ModuleReader.java b/HMCLCore/src/main/java/java/lang/module/ModuleReader.java new file mode 100644 index 000000000..09fe4ab5d --- /dev/null +++ b/HMCLCore/src/main/java/java/lang/module/ModuleReader.java @@ -0,0 +1,18 @@ +package java.lang.module; + +import java.io.Closeable; +import java.io.IOException; +import java.util.stream.Stream; + +/** + * Dummy java compatibility class + * + * @author xxDark + */ +public interface ModuleReader extends Closeable { + //CHECKSTYLE:OFF + Stream list() throws IOException; + @Override + void close() throws IOException; + //CHECKSTYLE:ON +} diff --git a/HMCLCore/src/main/java/java/lang/module/ModuleReference.java b/HMCLCore/src/main/java/java/lang/module/ModuleReference.java new file mode 100644 index 000000000..0d8007eb8 --- /dev/null +++ b/HMCLCore/src/main/java/java/lang/module/ModuleReference.java @@ -0,0 +1,14 @@ +package java.lang.module; + +import java.io.IOException; + +/** + * Dummy java compatibility class + * + * @author xxDark + */ +public abstract class ModuleReference { + //CHECKSTYLE:OFF + public abstract ModuleReader open() throws IOException; + //CHECKSTYLE:ON +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModInfo.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModInfo.java index f65b6d875..d93ce5d55 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModInfo.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModInfo.java @@ -167,6 +167,10 @@ public final class ModInfo implements Comparable { private final String text; private final String color; + public Part(String text) { + this(text, ""); + } + public Part(String text, String color) { this.text = Objects.requireNonNull(text); this.color = Objects.requireNonNull(color); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ClassUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ClassUtils.java new file mode 100644 index 000000000..1b4a67f6a --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ClassUtils.java @@ -0,0 +1,13 @@ +package org.jackhuang.hmcl.util; + +/** + * Utilities for dealing with class-file loading/parsing. + * + * @author Matt + */ +public class ClassUtils { + /** + * The offset from which a version and the version constant value is. For example, Java 8 is 52 (44 + 8). + */ + public static final int VERSION_OFFSET = 44; +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Java9Util.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Java9Util.java new file mode 100644 index 000000000..b3a43b4fe --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Java9Util.java @@ -0,0 +1,65 @@ +package org.jackhuang.hmcl.util; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; + +/** + * Package-private util to deal with modules. + * + * @author xxDark + */ +final class Java9Util { + + private static final MethodHandle CLASS_MODULE; + private static final MethodHandle CLASS_LOADER_MDOULE; + + /** + * Deny all constructions. + */ + private Java9Util() { + } + + /** + * @param klass {@link Class} to get module from. + * @return {@link Module} of the class. + */ + static Module getClassModule(Class klass) { + try { + return (Module) CLASS_MODULE.invokeExact(klass); + } catch (Throwable t) { + // That should never happen. + throw new AssertionError(t); + } + } + + + /** + * @param loader {@link ClassLoader} to get module from. + * @return {@link Module} of the class. + */ + static Module getLoaderModule(ClassLoader loader) { + try { + return (Module) CLASS_LOADER_MDOULE.invokeExact(loader); + } catch (Throwable t) { + // That should never happen. + throw new AssertionError(t); + } + } + + static { + try { + Field field = Lookup.class.getDeclaredField("IMPL_LOOKUP"); + field.setAccessible(true); + MethodHandles.publicLookup(); + Lookup lookup = (Lookup) field.get(null); + MethodType type = MethodType.methodType(Module.class); + CLASS_MODULE = lookup.findVirtual(Class.class, "getModule", type); + CLASS_LOADER_MDOULE = lookup.findVirtual(ClassLoader.class, "getUnnamedModule", type); + } catch (NoSuchMethodException | IllegalAccessException | NoSuchFieldException ex) { + throw new ExceptionInInitializerError(ex); + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/VMUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/VMUtils.java new file mode 100644 index 000000000..55a3081af --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/VMUtils.java @@ -0,0 +1,270 @@ +package org.jackhuang.hmcl.util; + +import com.sun.javafx.application.PlatformImpl; +import javafx.application.Platform; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashMap; +import java.util.HashSet; +import java.util.logging.Level; + +/** + * Dependent and non-dependent platform utilities for VM. + * + * @author xxDark + */ +public final class VMUtils { + private static int vmVersion = -1; + + /** + * Deny all constructions. + */ + private VMUtils() { } + + /** + * Appends URL to the {@link URLClassLoader}. + * + * @param cl the classloader to add {@link URL} for. + * @param url the {@link URL} to add. + */ + public static void addURL(ClassLoader cl, URL url) { + if (cl instanceof URLClassLoader) { + addURL0(cl, url); + } else { + addURL1(cl, url); + } + } + + /** + * @return running VM version. + */ + public static int getVmVersion() { + if (vmVersion < 0) { + // Check for class version, ez + String property = System.getProperty("java.class.version", ""); + if (!property.isEmpty()) + return vmVersion = (int) (Float.parseFloat(property) - ClassUtils.VERSION_OFFSET); + // Odd, not found. Try the spec version + Logging.LOG.warning("Using fallback vm-version fetch, no value for 'java.class.version'"); + property = System.getProperty("java.vm.specification.version", ""); + if (property.contains(".")) + return vmVersion = (int) Float.parseFloat(property.substring(property.indexOf('.') + 1)); + else if (!property.isEmpty()) + return vmVersion = Integer.parseInt(property); + // Very odd + Logging.LOG.warning("Fallback vm-version fetch failed, defaulting to 8"); + return 8; + } + return vmVersion; + } + + private static void addURL0(ClassLoader loader, URL url) { + Method method; + try { + method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + } catch (NoSuchMethodException ex) { + throw new RuntimeException("No 'addURL' method in java.net.URLClassLoader", ex); + } + method.setAccessible(true); + try { + method.invoke(loader, url); + } catch (IllegalAccessException ex) { + throw new IllegalStateException("'addURL' became inaccessible", ex); + } catch (InvocationTargetException ex) { + throw new RuntimeException("Error adding URL", ex.getTargetException()); + } + } + + private static void addURL1(ClassLoader loader, URL url) { + Class currentClass = loader.getClass(); + do { + Field field; + try { + field = currentClass.getDeclaredField("ucp"); + } catch (NoSuchFieldException ignored) { + continue; + } + field.setAccessible(true); + Object ucp; + try { + ucp = field.get(loader); + } catch (IllegalAccessException ex) { + throw new IllegalStateException("'ucp' became inaccessible", ex); + } + String className; + if (getVmVersion() < 9) { + className = "sun.misc.URLClassPath"; + } else { + className = "jdk.internal.misc.URLClassPath"; + } + Method method; + try { + method = Class.forName(className, true, null).getDeclaredMethod("addURL", URL.class); + } catch (NoSuchMethodException ex) { + throw new RuntimeException("No 'addURL' method in " + className, ex); + } catch (ClassNotFoundException ex) { + throw new RuntimeException(className + " was not found", ex); + } + method.setAccessible(true); + try { + method.invoke(ucp, url); + break; + } catch (IllegalAccessException ex) { + throw new IllegalStateException("'addURL' became inaccessible", ex); + } catch (InvocationTargetException ex) { + throw new RuntimeException("Error adding URL", ex.getTargetException()); + } + } while ((currentClass=currentClass.getSuperclass()) != Object.class); + throw new IllegalArgumentException("No 'ucp' field in " + loader); + } + + /** + * Closes {@link URLClassLoader}. + * + * @param loader + * Loader to close. + * + * @throws IOException + * When I/O error occurs. + */ + public static void close(URLClassLoader loader) throws IOException { + loader.close(); + } + + /** + * Sets parent class loader. + * + * @param loader + * Loader to change parent for. + * @param parent + * New parent loader. + */ + public static void setParent(ClassLoader loader, ClassLoader parent) { + Field field; + try { + field = ClassLoader.class.getDeclaredField("parent"); + } catch (NoSuchFieldException ex) { + throw new RuntimeException("No 'parent' field in java.lang.ClassLoader", ex); + } + field.setAccessible(true); + try { + field.set(loader, parent); + } catch (IllegalAccessException ex) { + throw new IllegalStateException("'parent' became inaccessible", ex); + } + } + + /** + * Initializes toolkit. + */ + public static void tkIint() { + if (getVmVersion() < 9) { + PlatformImpl.startup(() -> {}); + } else { + Method m; + try { + m = Platform.class.getDeclaredMethod("startup", Runnable.class); + } catch (NoSuchMethodException ex) { + throw new RuntimeException("javafx.application.Platform.startup(Runnable) is missing", ex); + } + m.setAccessible(true); + try { + m.invoke(null, (Runnable) () -> {}); + } catch (IllegalAccessException ex) { + throw new IllegalStateException("'startup' became inaccessible", ex); + } catch (InvocationTargetException ex) { + throw new RuntimeException("Unable to initialize toolkit", ex.getTargetException()); + } + } + } + + /** + * Patches JDK stuff. + */ + public static void patch() { + if (getVmVersion() > 8) { + openPackages(); + patchReflectionFilters(); + } + } + + /** + * Opens all packages. + */ + private static void openPackages() { + try { + Method export = Module.class.getDeclaredMethod("implAddOpens", String.class); + export.setAccessible(true); + HashSet modules = new HashSet<>(); + Class classBase = VMUtils.class; + Module base = Java9Util.getClassModule(classBase); + if (base.getLayer() != null) + modules.addAll(base.getLayer().modules()); + modules.addAll(ModuleLayer.boot().modules()); + for (ClassLoader cl = classBase.getClassLoader(); cl != null; cl = cl.getParent()) { + modules.add(Java9Util.getLoaderModule(cl)); + } + for (Module module : modules) { + for (String name : module.getPackages()) { + try { + export.invoke(module, name); + } catch (Exception ex) { + Logging.LOG.log(Level.SEVERE, "Could not export package " + name + " in module " + module, ex); + } + } + } + } catch (Exception ex) { + Logging.LOG.log(Level.SEVERE, "Could not export packages", ex); + } + } + + /** + * Patches reflection filters. + */ + private static void patchReflectionFilters() { + Class klass; + try { + klass = Class.forName("jdk.internal.reflect.Reflection", + true, null); + } catch (ClassNotFoundException ex) { + throw new RuntimeException("Unable to locate 'jdk.internal.reflect.Reflection' class", ex); + } + try { + Field[] fields; + try { + Method m = Class.class.getDeclaredMethod("getDeclaredFieldsImpl"); + m.setAccessible(true); + fields = (Field[]) m.invoke(klass); + } catch (NoSuchMethodException | InvocationTargetException ex) { + try { + Method m = Class.class.getDeclaredMethod("getDeclaredFields0", Boolean.TYPE); + m.setAccessible(true); + fields = (Field[]) m.invoke(klass, false); + } catch (InvocationTargetException | NoSuchMethodException ex1) { + ex.addSuppressed(ex1); + throw new RuntimeException("Unable to get all class fields", ex); + } + } + int c = 0; + for (Field field : fields) { + String name = field.getName(); + if ("fieldFilterMap".equals(name) || "methodFilterMap".equals(name)) { + field.setAccessible(true); + field.set(null, new HashMap<>(0)); + if (++c == 2) { + return; + } + } + } + throw new RuntimeException("One of field patches did not apply properly. " + + "Expected to patch two fields, but patched: " + c); + } catch (IllegalAccessException ex) { + throw new RuntimeException("Unable to patch reflection filters", ex); + } + } +}