From 9c9aa8fa5bd9df0ed86470c89dd4b3c93030a5e6 Mon Sep 17 00:00:00 2001 From: comp500 Date: Thu, 2 Jul 2020 21:41:42 +0100 Subject: [PATCH] Rewrite jimfs compat to work on OpenJ9, and in a cleaner way Instead of resetting the FileSystemProvider list and changing the system classloader so that it is found properly when reloading, run ServiceLoader on our own classloader and add providers to the existing list (copying into an ArrayList to allow modifiability) Also refactored some code to make the intent clearer. --- build.gradle | 2 +- gradle.properties | 2 +- .../link/infra/jumploader/Jumploader.java | 24 +--- .../FabricLoaderReflectionHack.java | 107 ------------------ .../FileSystemProviderAppender.java | 57 ++++++++++ .../specialcases/JimfsReflectionHack.java | 40 +++++++ .../specialcases/SpecialCaseHandler.java | 5 +- .../infra/jumploader/util/ReflectionUtil.java | 29 +++++ src/main/resources/fabric.mod.json | 2 +- 9 files changed, 136 insertions(+), 132 deletions(-) delete mode 100644 src/main/java/link/infra/jumploader/specialcases/FabricLoaderReflectionHack.java create mode 100644 src/main/java/link/infra/jumploader/specialcases/FileSystemProviderAppender.java create mode 100644 src/main/java/link/infra/jumploader/specialcases/JimfsReflectionHack.java create mode 100644 src/main/java/link/infra/jumploader/util/ReflectionUtil.java diff --git a/build.gradle b/build.gradle index c8fbbfa..a4c644f 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ apply plugin: 'net.minecraftforge.gradle' apply plugin: 'eclipse' apply plugin: 'maven-publish' -version = '1.0.11' +version = '1.0.12' group = 'link.infra.jumploader' // http://maven.apache.org/guides/mini/guide-naming-conventions.html archivesBaseName = 'jumploader' diff --git a/gradle.properties b/gradle.properties index f677e6e..5fb7613 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ modGroup=link.infra.jumploader -modVersion=1.0.11 +modVersion=1.0.12 modBaseName=jumploader diff --git a/src/main/java/link/infra/jumploader/Jumploader.java b/src/main/java/link/infra/jumploader/Jumploader.java index 782e085..75163d8 100644 --- a/src/main/java/link/infra/jumploader/Jumploader.java +++ b/src/main/java/link/infra/jumploader/Jumploader.java @@ -15,6 +15,7 @@ import link.infra.jumploader.specialcases.ReflectionHack; import link.infra.jumploader.specialcases.SpecialCaseHandler; import link.infra.jumploader.ui.GUIManager; +import link.infra.jumploader.util.ReflectionUtil; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -24,7 +25,6 @@ import java.io.IOException; import java.io.PrintStream; import java.io.PrintWriter; -import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; @@ -35,7 +35,7 @@ import java.util.Set; public class Jumploader implements ITransformationService { - public static final String VERSION = "1.0.11"; + public static final String VERSION = "1.0.12"; public static final String USER_AGENT = "Jumploader/" + VERSION; private final Logger LOGGER = LogManager.getLogger(); @@ -56,32 +56,16 @@ public void beginScanning(@Nonnull IEnvironment env) { throw new RuntimeException("Jumploader shouldn't get to this stage!"); } - @SuppressWarnings("unchecked") - private static T reflectField(Object destObj, String name) throws NoSuchFieldException, IllegalAccessException, ClassCastException { - Field field = destObj.getClass().getDeclaredField(name); - field.setAccessible(true); - return (T) field.get(destObj); - } - @Override public void onLoad(@Nonnull IEnvironment env, @Nonnull Set set) { LOGGER.info("Jumploader " + VERSION + " initialising, discovering environment..."); - // OpenJ9 is currently unsupported - it does not allow bypassing the reflection blacklist to change the system classloader - // See FabricLoaderReflectionHack.java - if (System.getProperty("java.vm.name", "").toLowerCase().contains("openj9")) { - LOGGER.fatal("OpenJ9 is currently unsupported with Jumploader"); - LOGGER.fatal("Please use Hotspot, or install your desired modloader directly instead of jumploading it!"); - LOGGER.fatal("See https://fabricmc.net/wiki/install for a Fabric installation guide"); - System.exit(1); - } - // Get the game arguments ParsedArguments argsParsedTemp; // Very bad reflection, don't try this at home!! try { - ArgumentHandler handler = reflectField(Launcher.INSTANCE, "argumentHandler"); - String[] gameArgs = reflectField(handler, "args"); + ArgumentHandler handler = ReflectionUtil.reflectField(Launcher.INSTANCE, "argumentHandler"); + String[] gameArgs = ReflectionUtil.reflectField(handler, "args"); if (gameArgs == null) { LOGGER.warn("Game arguments are null, have they been parsed yet?"); gameArgs = new String[0]; diff --git a/src/main/java/link/infra/jumploader/specialcases/FabricLoaderReflectionHack.java b/src/main/java/link/infra/jumploader/specialcases/FabricLoaderReflectionHack.java deleted file mode 100644 index 0087afd..0000000 --- a/src/main/java/link/infra/jumploader/specialcases/FabricLoaderReflectionHack.java +++ /dev/null @@ -1,107 +0,0 @@ -package link.infra.jumploader.specialcases; - -import link.infra.jumploader.resources.ParsedArguments; -import link.infra.jumploader.util.RegexUtil; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; -import java.lang.reflect.Field; -import java.net.URL; -import java.net.URLStreamHandler; -import java.nio.file.spi.FileSystemProvider; -import java.util.Hashtable; -import java.util.List; -import java.util.regex.Pattern; - -public class FabricLoaderReflectionHack implements ReflectionHack { - private final Logger LOGGER = LogManager.getLogger(); - private static final double JAVA_VERSION = Double.parseDouble(System.getProperty("java.specification.version", "0")); - - private interface ReflectionVersionHandler { - void setClassLoader(ClassLoader loader) throws NoSuchFieldException, IllegalAccessException; - void resetInstalledProviders() throws NoSuchFieldException, IllegalAccessException; - } - - private static class ReflectionVersionHandler8 implements ReflectionVersionHandler { - @Override - public void setClassLoader(ClassLoader loader) throws NoSuchFieldException, IllegalAccessException { - Field scl = ClassLoader.class.getDeclaredField("scl"); - scl.setAccessible(true); - scl.set(null, loader); - } - - @Override - public void resetInstalledProviders() throws NoSuchFieldException, IllegalAccessException { - Field installedProviders = FileSystemProvider.class.getDeclaredField("installedProviders"); - installedProviders.setAccessible(true); - installedProviders.set(null, null); - Field loadingProviders = FileSystemProvider.class.getDeclaredField("loadingProviders"); - loadingProviders.setAccessible(true); - loadingProviders.set(null, false); - } - } - - @SuppressWarnings("Since15") - private static class ReflectionVersionHandler9 implements ReflectionVersionHandler { - @Override - public void setClassLoader(ClassLoader loader) throws NoSuchFieldException, IllegalAccessException { - VarHandle handle = MethodHandles.privateLookupIn(ClassLoader.class, MethodHandles.lookup()).findStaticVarHandle(ClassLoader.class, "scl", ClassLoader.class); - handle.set(loader); - } - - @Override - public void resetInstalledProviders() throws NoSuchFieldException, IllegalAccessException { - MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(FileSystemProvider.class, MethodHandles.lookup()); - VarHandle installedProviders = lookup.findStaticVarHandle(FileSystemProvider.class, "installedProviders", List.class); - installedProviders.set((Object) null); - VarHandle loadingProviders = lookup.findStaticVarHandle(FileSystemProvider.class, "loadingProviders", boolean.class); - loadingProviders.set(false); - } - } - - private static void reloadFSHandlers(ClassLoader classLoader) throws NoSuchFieldException, IllegalAccessException { - // Attempt to load the jimfs protocol handler (required for jar-in-jar) by hacking around the system classloader - ClassLoader existingLoader = ClassLoader.getSystemClassLoader(); - ReflectionVersionHandler handler; - if (JAVA_VERSION > 8) { - handler = new ReflectionVersionHandler9(); - } else { - handler = new ReflectionVersionHandler8(); - } - handler.setClassLoader(classLoader); - - // Force FileSystemProvider to re-enumerate installed providers - handler.resetInstalledProviders(); - FileSystemProvider.installedProviders(); - - // Set the system classloader back to the actual system classloader - handler.setClassLoader(existingLoader); - } - - @SuppressWarnings("unchecked") - private static void injectJimfsHandler(ClassLoader classLoader) throws NoSuchFieldException, IllegalAccessException, ClassCastException, ClassNotFoundException, InstantiationException { - // Add the jimfs handler to the URL handlers field, because Class.forName by default uses the classloader that loaded the calling class (in this case the system classloader, so we have to do it manually) - Field handlersField = URL.class.getDeclaredField("handlers"); - handlersField.setAccessible(true); - Hashtable handlers = (Hashtable) handlersField.get(null); - handlers.putIfAbsent("jimfs", (URLStreamHandler) Class.forName("com.google.common.jimfs.Handler", true, classLoader).newInstance()); - } - - @Override - public void applyReflectionHack(ClassLoader loadingClassloader) { - // Jimfs requires some funky hacks to load under a custom classloader, Java protocol handlers don't handle custom classloaders very well - try { - FabricLoaderReflectionHack.reloadFSHandlers(loadingClassloader); - FabricLoaderReflectionHack.injectJimfsHandler(loadingClassloader); - } catch (NoSuchFieldException | IllegalAccessException | ClassNotFoundException | InstantiationException | ClassCastException e) { - LOGGER.warn("Failed to fix jimfs loading, jar-in-jar may not work", e); - } - } - - @Override - public boolean shouldApply(List loadedJars, String mainClass, ParsedArguments gameArguments) { - return RegexUtil.patternMatchesJars(Pattern.compile("fabric-loader-(.+)\\.jar$"), loadedJars); - } -} diff --git a/src/main/java/link/infra/jumploader/specialcases/FileSystemProviderAppender.java b/src/main/java/link/infra/jumploader/specialcases/FileSystemProviderAppender.java new file mode 100644 index 0000000..d71c123 --- /dev/null +++ b/src/main/java/link/infra/jumploader/specialcases/FileSystemProviderAppender.java @@ -0,0 +1,57 @@ +package link.infra.jumploader.specialcases; + +import link.infra.jumploader.resources.ParsedArguments; +import link.infra.jumploader.util.ReflectionUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.net.URL; +import java.nio.file.spi.FileSystemProvider; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; + +public class FileSystemProviderAppender implements ReflectionHack { + private final Logger LOGGER = LogManager.getLogger(); + + private void loadProvidersFromClassLoader(ClassLoader classLoader, List list) { + ServiceLoader loader = ServiceLoader.load(FileSystemProvider.class, classLoader); + + for (FileSystemProvider provider: loader) { + String scheme = provider.getScheme(); + if (!scheme.equalsIgnoreCase("file")) { + if (list.stream().noneMatch(p -> p.getScheme().equalsIgnoreCase(scheme))) { + list.add(provider); + } + } + } + } + + @Override + public void applyReflectionHack(ClassLoader loadingClassloader) { + // Ensure the existing providers are loaded first + FileSystemProvider.installedProviders(); + + try { + final Object lock = ReflectionUtil.reflectStaticField(FileSystemProvider.class, "lock"); + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (lock) { + // Load providers from the Jumploader classloader, and add them to the FileSystemProvider list + ReflectionUtil.>transformStaticField(FileSystemProvider.class, "installedProviders", existingProviders -> { + List newList = new ArrayList<>(existingProviders); + AccessController.doPrivileged((PrivilegedAction) () -> {loadProvidersFromClassLoader(loadingClassloader, newList); return null;}); + return newList; + }); + } + } catch (NoSuchFieldException | IllegalAccessException | ClassCastException e) { + LOGGER.warn("Failed to fix FileSystemProvider loading, jar-in-jar may not work", e); + } + } + + @Override + public boolean shouldApply(List loadedJars, String mainClass, ParsedArguments gameArguments) { + return true; + } +} diff --git a/src/main/java/link/infra/jumploader/specialcases/JimfsReflectionHack.java b/src/main/java/link/infra/jumploader/specialcases/JimfsReflectionHack.java new file mode 100644 index 0000000..28f4738 --- /dev/null +++ b/src/main/java/link/infra/jumploader/specialcases/JimfsReflectionHack.java @@ -0,0 +1,40 @@ +package link.infra.jumploader.specialcases; + +import link.infra.jumploader.resources.ParsedArguments; +import link.infra.jumploader.util.ReflectionUtil; +import link.infra.jumploader.util.RegexUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLStreamHandler; +import java.util.Hashtable; +import java.util.List; +import java.util.regex.Pattern; + +public class JimfsReflectionHack implements ReflectionHack { + private final Logger LOGGER = LogManager.getLogger(); + + private static void injectJimfsHandler(ClassLoader classLoader) throws NoSuchFieldException, IllegalAccessException, ClassCastException, ClassNotFoundException, InstantiationException, NoSuchMethodException, InvocationTargetException { + // Add the jimfs handler to the URL handlers field, because Class.forName by default uses the classloader that loaded the calling class (in this case the system classloader, so we have to do it manually) + Hashtable handlers = ReflectionUtil.reflectStaticField(URL.class, "handlers"); + handlers.putIfAbsent("jimfs", (URLStreamHandler) Class.forName("com.google.common.jimfs.Handler", true, classLoader).getDeclaredConstructor().newInstance()); + } + + @Override + public void applyReflectionHack(ClassLoader loadingClassloader) { + // Jimfs requires some funky hacks to load under a custom classloader, Java protocol handlers don't handle custom classloaders very well + // See also FileSystemProviderAppender + try { + JimfsReflectionHack.injectJimfsHandler(loadingClassloader); + } catch (NoSuchFieldException | IllegalAccessException | ClassNotFoundException | InstantiationException | ClassCastException | NoSuchMethodException | InvocationTargetException e) { + LOGGER.warn("Failed to fix jimfs loading, jar-in-jar may not work", e); + } + } + + @Override + public boolean shouldApply(List loadedJars, String mainClass, ParsedArguments gameArguments) { + return RegexUtil.patternMatchesJars(Pattern.compile("jimfs-(.+)\\.jar$"), loadedJars); + } +} diff --git a/src/main/java/link/infra/jumploader/specialcases/SpecialCaseHandler.java b/src/main/java/link/infra/jumploader/specialcases/SpecialCaseHandler.java index 2fa302d..bf687b3 100644 --- a/src/main/java/link/infra/jumploader/specialcases/SpecialCaseHandler.java +++ b/src/main/java/link/infra/jumploader/specialcases/SpecialCaseHandler.java @@ -9,11 +9,12 @@ public class SpecialCaseHandler { private final List allCases = Arrays.asList( - new FabricLoaderReflectionHack(), + new JimfsReflectionHack(), new MixinHideModLauncherBlacklist(), new ServerSideRemoveFMLArgs(), new ClassRedefinerASM(), - new ForgeJarClasspathModifier() + new ForgeJarClasspathModifier(), + new FileSystemProviderAppender() ); private final List appliedCases = new ArrayList<>(); diff --git a/src/main/java/link/infra/jumploader/util/ReflectionUtil.java b/src/main/java/link/infra/jumploader/util/ReflectionUtil.java new file mode 100644 index 0000000..f5ce93d --- /dev/null +++ b/src/main/java/link/infra/jumploader/util/ReflectionUtil.java @@ -0,0 +1,29 @@ +package link.infra.jumploader.util; + +import java.lang.reflect.Field; +import java.util.function.Function; + +public class ReflectionUtil { + @SuppressWarnings("unchecked") + public static T reflectField(Object destObj, String name) throws NoSuchFieldException, IllegalAccessException, ClassCastException { + Field field = destObj.getClass().getDeclaredField(name); + field.setAccessible(true); + return (T) field.get(destObj); + } + + @SuppressWarnings("unchecked") + public static T reflectStaticField(Class destClass, String name) throws NoSuchFieldException, IllegalAccessException, ClassCastException { + Field field = destClass.getDeclaredField(name); + field.setAccessible(true); + return (T) field.get(null); + } + + @SuppressWarnings({"unchecked", "UnusedReturnValue"}) + public static T transformStaticField(Class destClass, String name, Function transformer) throws NoSuchFieldException, IllegalAccessException, ClassCastException { + Field field = destClass.getDeclaredField(name); + field.setAccessible(true); + T value = transformer.apply((T) field.get(null)); + field.set(null, value); + return value; + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index b75ac79..249f9bd 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "id": "jumploader", - "version": "1.0.11", + "version": "1.0.12", "name": "Jumploader", "description": "Allows the use of Fabric mods in Twitch modpacks, by loading Fabric as if it were a Forge mod.",