diff --git a/common/src/main/java/me/lucko/luckperms/common/extension/SimpleExtensionManager.java b/common/src/main/java/me/lucko/luckperms/common/extension/SimpleExtensionManager.java index f271e50ea..9a0143c9b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/extension/SimpleExtensionManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/extension/SimpleExtensionManager.java @@ -28,7 +28,7 @@ package me.lucko.luckperms.common.extension; import com.google.gson.JsonObject; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import me.lucko.luckperms.common.plugin.classpath.ReflectionClassPathAppender; +import me.lucko.luckperms.common.plugin.classpath.URLClassLoaderAccess; import me.lucko.luckperms.common.util.gson.GsonProvider; import net.luckperms.api.LuckPerms; @@ -206,7 +206,7 @@ public class SimpleExtensionManager implements ExtensionManager, AutoCloseable { throw new RuntimeException("useParentClassLoader is true but parent is not a URLClassLoader"); } - ReflectionClassPathAppender.addUrl(((URLClassLoader) parentClassLoader), path.toUri().toURL()); + URLClassLoaderAccess.create(((URLClassLoader) parentClassLoader)).addURL(path.toUri().toURL()); } private static final class LoadedExtension { diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/ReflectionClassPathAppender.java b/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/ReflectionClassPathAppender.java index 2aec61edb..11cdb8f97 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/ReflectionClassPathAppender.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/ReflectionClassPathAppender.java @@ -27,56 +27,17 @@ package me.lucko.luckperms.common.plugin.classpath; import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; -import java.lang.reflect.Method; import java.net.MalformedURLException; -import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; -@Deprecated // TODO: no longer works on Java 16 - Sponge needs to switch to JarInJar or find an API to add to classpath at runtime public class ReflectionClassPathAppender implements ClassPathAppender { - private static final Method ADD_URL_METHOD; - - static { - // If on Java 9+, open the URLClassLoader module to this module - // so we can access its API via reflection without producing a warning. - try { - openUrlClassLoaderModule(); - } catch (Throwable e) { - // ignore - } - - try { - ADD_URL_METHOD = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - - try { - ADD_URL_METHOD.setAccessible(true); - } catch (Throwable e) { - new RuntimeException("LuckPerms is unable to access the URLClassLoader#addURL method using reflection. \n" + - "You may be able to fix this problem by adding the following command-line argument " + - "directly after the 'java' command in your start script: \n'--add-opens java.base/java.lang=ALL-UNNAMED'", e).printStackTrace(); - } - } - - /** - * Adds the given {@link URL} to the class loader. - * - * @param classLoader the class loader - * @param url the url to add - */ - public static void addUrl(URLClassLoader classLoader, URL url) throws ReflectiveOperationException { - ADD_URL_METHOD.invoke(classLoader, url); - } - - private final URLClassLoader classLoader; + private final URLClassLoaderAccess classLoaderAccess; public ReflectionClassPathAppender(LuckPermsBootstrap bootstrap) throws IllegalStateException { ClassLoader classLoader = bootstrap.getClass().getClassLoader(); if (classLoader instanceof URLClassLoader) { - this.classLoader = (URLClassLoader) classLoader; + this.classLoaderAccess = URLClassLoaderAccess.create((URLClassLoader) classLoader); } else { throw new IllegalStateException("ClassLoader is not instance of URLClassLoader"); } @@ -85,29 +46,10 @@ public class ReflectionClassPathAppender implements ClassPathAppender { @Override public void addJarToClasspath(Path file) { try { - addUrl(this.classLoader, file.toUri().toURL()); - } catch (ReflectiveOperationException | MalformedURLException e) { + this.classLoaderAccess.addURL(file.toUri().toURL()); + } catch (MalformedURLException e) { throw new RuntimeException(e); } } - private static void openUrlClassLoaderModule() throws Exception { - // This is effectively calling: - // - // URLClassLoader.class.getModule().addOpens( - // URLClassLoader.class.getPackageName(), - // ReflectionClassLoader.class.getModule() - // ); - // - // We use reflection since we build against Java 8. - - Class moduleClass = Class.forName("java.lang.Module"); - Method getModuleMethod = Class.class.getMethod("getModule"); - Method addOpensMethod = moduleClass.getMethod("addOpens", String.class, moduleClass); - - Object urlClassLoaderModule = getModuleMethod.invoke(URLClassLoader.class); - Object thisModule = getModuleMethod.invoke(ReflectionClassPathAppender.class); - - addOpensMethod.invoke(urlClassLoaderModule, URLClassLoader.class.getPackage().getName(), thisModule); - } } diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/URLClassLoaderAccess.java b/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/URLClassLoaderAccess.java new file mode 100644 index 000000000..6cdc7a7d0 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/URLClassLoaderAccess.java @@ -0,0 +1,188 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.plugin.classpath; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Collection; + +/** + * Provides access to {@link URLClassLoader}#addURL. + */ +public abstract class URLClassLoaderAccess { + + /** + * Creates a {@link URLClassLoaderAccess} for the given class loader. + * + * @param classLoader the class loader + * @return the access object + */ + public static URLClassLoaderAccess create(URLClassLoader classLoader) { + if (Reflection.isSupported()) { + return new Reflection(classLoader); + } else if (Unsafe.isSupported()) { + return new Unsafe(classLoader); + } else { + return Noop.INSTANCE; + } + } + + private final URLClassLoader classLoader; + + protected URLClassLoaderAccess(URLClassLoader classLoader) { + this.classLoader = classLoader; + } + + + /** + * Adds the given URL to the class loader. + * + * @param url the URL to add + */ + public abstract void addURL(@NonNull URL url); + + private static void throwError(Throwable cause) throws UnsupportedOperationException { + throw new UnsupportedOperationException("LuckPerms is unable to inject into the plugin URLClassLoader.\n" + + "You may be able to fix this problem by adding the following command-line argument " + + "directly after the 'java' command in your start script: \n'--add-opens java.base/java.lang=ALL-UNNAMED'", cause); + } + + /** + * Accesses using reflection, not supported on Java 9+. + */ + private static class Reflection extends URLClassLoaderAccess { + private static final Method ADD_URL_METHOD; + + static { + Method addUrlMethod; + try { + addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + addUrlMethod.setAccessible(true); + } catch (Exception e) { + addUrlMethod = null; + } + ADD_URL_METHOD = addUrlMethod; + } + + private static boolean isSupported() { + return ADD_URL_METHOD != null; + } + + Reflection(URLClassLoader classLoader) { + super(classLoader); + } + + @Override + public void addURL(@NonNull URL url) { + try { + ADD_URL_METHOD.invoke(super.classLoader, url); + } catch (ReflectiveOperationException e) { + URLClassLoaderAccess.throwError(e); + } + } + } + + /** + * Accesses using sun.misc.Unsafe, supported on Java 9+. + * + * @author Vaishnav Anil (https://github.com/slimjar/slimjar) + */ + private static class Unsafe extends URLClassLoaderAccess { + private static final sun.misc.Unsafe UNSAFE; + + static { + sun.misc.Unsafe unsafe; + try { + Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + unsafe = (sun.misc.Unsafe) unsafeField.get(null); + } catch (Throwable t) { + unsafe = null; + } + UNSAFE = unsafe; + } + + private static boolean isSupported() { + return UNSAFE != null; + } + + private final Collection unopenedURLs; + private final Collection pathURLs; + + @SuppressWarnings("unchecked") + Unsafe(URLClassLoader classLoader) { + super(classLoader); + + Collection unopenedURLs; + Collection pathURLs; + try { + Object ucp = fetchField(URLClassLoader.class, classLoader, "ucp"); + unopenedURLs = (Collection) fetchField(ucp.getClass(), ucp, "unopenedUrls"); + pathURLs = (Collection) fetchField(ucp.getClass(), ucp, "path"); + } catch (Throwable e) { + unopenedURLs = null; + pathURLs = null; + } + + this.unopenedURLs = unopenedURLs; + this.pathURLs = pathURLs; + } + + private static Object fetchField(final Class clazz, final Object object, final String name) throws NoSuchFieldException { + Field field = clazz.getDeclaredField(name); + long offset = UNSAFE.objectFieldOffset(field); + return UNSAFE.getObject(object, offset); + } + + @Override + public void addURL(@NonNull URL url) { + if (this.unopenedURLs == null || this.pathURLs == null) { + URLClassLoaderAccess.throwError(new NullPointerException("unopenedURLs or pathURLs")); + } + + this.unopenedURLs.add(url); + this.pathURLs.add(url); + } + } + + private static class Noop extends URLClassLoaderAccess { + private static final Noop INSTANCE = new Noop(); + + private Noop() { + super(null); + } + + @Override + public void addURL(@NonNull URL url) { + URLClassLoaderAccess.throwError(null); + } + } + +}