diff --git a/fml/patches/minecraft/net/minecraft/crash/CrashReport.java.patch b/fml/patches/minecraft/net/minecraft/crash/CrashReport.java.patch index 0e8441a1d..224cdfd43 100644 --- a/fml/patches/minecraft/net/minecraft/crash/CrashReport.java.patch +++ b/fml/patches/minecraft/net/minecraft/crash/CrashReport.java.patch @@ -8,3 +8,12 @@ } public String func_71501_a() +@@ -220,6 +221,8 @@ + { + StringBuilder stringbuilder = new StringBuilder(); + stringbuilder.append("---- Minecraft Crash Report ----\n"); ++ net.minecraftforge.fml.common.asm.transformers.BlamingTransformer.onCrash(stringbuilder); ++ net.minecraftforge.fml.relauncher.CoreModManager.onCrash(stringbuilder); + stringbuilder.append("// "); + stringbuilder.append(func_71503_h()); + stringbuilder.append("\n\n"); diff --git a/fml/src/main/java/net/minecraftforge/fml/common/FMLCommonHandler.java b/fml/src/main/java/net/minecraftforge/fml/common/FMLCommonHandler.java index 391ecf966..003cb067e 100644 --- a/fml/src/main/java/net/minecraftforge/fml/common/FMLCommonHandler.java +++ b/fml/src/main/java/net/minecraftforge/fml/common/FMLCommonHandler.java @@ -47,6 +47,7 @@ import net.minecraftforge.fml.common.gameevent.PlayerEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; import net.minecraftforge.fml.common.gameevent.TickEvent.Phase; import net.minecraftforge.fml.common.network.NetworkRegistry; +import net.minecraftforge.fml.relauncher.CoreModManager; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.server.FMLServerHandler; @@ -97,6 +98,28 @@ public class FMLCommonHandler private WeakReference handlerToCheck; private EventBus eventBus = new EventBus(); private volatile CountDownLatch exitLatch = null; + + private FMLCommonHandler() + { + registerCrashCallable(new ICrashCallable() + { + public String call() throws Exception + { + StringBuilder builder = new StringBuilder(); + Joiner joiner = Joiner.on("\n "); + for(String coreMod : CoreModManager.getTransformers().keySet()) + { + builder.append("\n" + coreMod + "\n ").append(joiner.join(CoreModManager.getTransformers().get(coreMod))); + } + return builder.toString(); + } + + public String getLabel() + { + return "Loaded coremods (and transformers)"; + } + }); + } /** * The FML event bus. Subscribe here for FML related events * diff --git a/fml/src/main/java/net/minecraftforge/fml/common/FMLModContainer.java b/fml/src/main/java/net/minecraftforge/fml/common/FMLModContainer.java index 0c06efb93..e4e6ae441 100644 --- a/fml/src/main/java/net/minecraftforge/fml/common/FMLModContainer.java +++ b/fml/src/main/java/net/minecraftforge/fml/common/FMLModContainer.java @@ -27,6 +27,7 @@ import java.util.Set; import net.minecraftforge.fml.common.Mod.Instance; import net.minecraftforge.fml.common.Mod.Metadata; +import net.minecraftforge.fml.common.asm.transformers.BlamingTransformer; import net.minecraftforge.fml.common.discovery.ASMDataTable; import net.minecraftforge.fml.common.discovery.ModCandidate; import net.minecraftforge.fml.common.discovery.ASMDataTable.ASMData; @@ -416,6 +417,7 @@ public class FMLModContainer implements ModContainer { try { + BlamingTransformer.addClasses(getModId(), candidate.getClassList()); ModClassLoader modClassLoader = event.getModClassLoader(); modClassLoader.addFile(source); modClassLoader.clearNegativeCacheFor(candidate.getClassList()); diff --git a/fml/src/main/java/net/minecraftforge/fml/common/ModClassLoader.java b/fml/src/main/java/net/minecraftforge/fml/common/ModClassLoader.java index b7bd56d3e..c0f60918f 100644 --- a/fml/src/main/java/net/minecraftforge/fml/common/ModClassLoader.java +++ b/fml/src/main/java/net/minecraftforge/fml/common/ModClassLoader.java @@ -14,19 +14,21 @@ package net.minecraftforge.fml.common; import java.io.File; import java.net.MalformedURLException; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; +import java.util.ArrayList; import java.util.List; import java.util.Set; -import org.apache.logging.log4j.Level; - import net.minecraft.launchwrapper.IClassTransformer; import net.minecraft.launchwrapper.LaunchClassLoader; import net.minecraftforge.fml.common.asm.transformers.ModAPITransformer; import net.minecraftforge.fml.common.discovery.ASMDataTable; +import org.apache.logging.log4j.Level; + import com.google.common.collect.ImmutableList; /** @@ -59,15 +61,18 @@ public class ModClassLoader extends URLClassLoader } public File[] getParentSources() { - List urls=mainClassLoader.getSources(); - File[] sources=new File[urls.size()]; try { - for (int i = 0; i files=new ArrayList(); + for(URL url : mainClassLoader.getSources()) { - sources[i]=new File(urls.get(i).toURI()); + URI uri = url.toURI(); + if(uri.getScheme() == "file") + { + files.add(new File(uri)); + } } - return sources; + return files.toArray(new File[]{}); } catch (URISyntaxException e) { diff --git a/fml/src/main/java/net/minecraftforge/fml/common/asm/ASMTransformerWrapper.java b/fml/src/main/java/net/minecraftforge/fml/common/asm/ASMTransformerWrapper.java new file mode 100644 index 000000000..4e14e6e4c --- /dev/null +++ b/fml/src/main/java/net/minecraftforge/fml/common/asm/ASMTransformerWrapper.java @@ -0,0 +1,263 @@ + + +package net.minecraftforge.fml.common.asm; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.security.Permission; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import net.minecraft.launchwrapper.IClassTransformer; +import net.minecraft.launchwrapper.LaunchClassLoader; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Maps; + +public class ASMTransformerWrapper +{ + private static final Map wrapperModMap = Maps.newHashMap(); + private static final Map wrapperParentMap = Maps.newHashMap(); + + private static final LoadingCache wrapperCache = CacheBuilder.newBuilder() + .maximumSize(30) + .weakValues() + .build(new CacheLoader() + { + public byte[] load(String file) throws Exception + { + return makeWrapper(file); + } + }); + + private static final URL asmGenRoot; + private static boolean injected = false; + + static + { + try + { + asmGenRoot = new URL("asmgen", null, -1, "/", new ASMGenHandler()); + } + catch(MalformedURLException e) + { + throw new RuntimeException(e); + } + } + + private static class ASMGenHandler extends URLStreamHandler + { + protected URLConnection openConnection(URL url) throws IOException + { + String file = url.getFile(); + if(file.equals("/")) + { + return new URLConnection(url) + { + public void connect() throws IOException + { + throw new UnsupportedOperationException(); + } + }; + } + if(!file.startsWith("/")) throw new RuntimeException("Malformed URL: " + url); + file = file.substring(1); + if(wrapperModMap.containsKey(file)) + { + return new ASMGenConnection(url, file); + } + return null; + } + } + + private static class ASMGenConnection extends URLConnection + { + private final String file; + + protected ASMGenConnection(URL url, String file) + { + super(url); + this.file = file; + } + + public void connect() throws IOException + { + throw new UnsupportedOperationException(); + } + + @Override + public InputStream getInputStream() + { + try + { + return new ByteArrayInputStream(wrapperCache.get(file)); + } + catch (ExecutionException e) + { + throw new RuntimeException(e); + } + } + + @Override + public Permission getPermission() + { + return null; + } + } + + public static String getTransformerWrapper(LaunchClassLoader launchLoader, String parentClass, String coreMod) + { + if(!injected) + { + injected = true; + launchLoader.addURL(asmGenRoot); + } + + String name = getWrapperName(parentClass); + String fileName = name.replace('.', '/') + ".class"; + wrapperModMap.put(fileName, coreMod); + wrapperParentMap.put(fileName, parentClass); + return name; + } + + private static byte[] makeWrapper(String fileName) + { + if(!wrapperModMap.containsKey(fileName) || !wrapperParentMap.containsKey(fileName) || !fileName.endsWith(".class")) + { + throw new IllegalArgumentException("makeWrapper called with strange argument: " + fileName); + } + String name = fileName.substring(0, fileName.length() - ".class".length()); + + try + { + Type wrapper = Type.getType(TransformerWrapper.class); + + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES); + + writer.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, name, null, wrapper.getInternalName(), null); + + Method m = Method.getMethod("void ()"); + GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC, m, null, null, writer); + mg.loadThis(); + mg.invokeConstructor(wrapper, m); + mg.returnValue(); + mg.endMethod(); + + m = Method.getMethod("java.lang.String getParentClass ()"); + mg = new GeneratorAdapter(Opcodes.ACC_PROTECTED, m, null, null, writer); + mg.push(wrapperParentMap.get(fileName)); + mg.returnValue(); + mg.endMethod(); + + m = Method.getMethod("java.lang.String getCoreMod ()"); + mg = new GeneratorAdapter(Opcodes.ACC_PROTECTED, m, null, null, writer); + mg.push(wrapperModMap.get(fileName)); + mg.returnValue(); + mg.endMethod(); + + writer.visitEnd(); + + return writer.toByteArray(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + private static String getWrapperName(String parentClass) + { + return parentClass + "_$Wrapper"; + } + + private static class WrapperVisitor extends ClassVisitor + { + private final String name; + private final String parentClass; + + public WrapperVisitor(ClassVisitor cv, String name, String parentClass) + { + super(Opcodes.ASM5, cv); + this.name = name.replace('.', '/'); + this.parentClass = parentClass; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) + { + super.visit(version, access, this.name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) + { + if(name.equals("parentClass")) + { + return super.visitField(access, name, desc, signature, parentClass); + } + return super.visitField(access, name, desc, signature, value); + } + } + + public static abstract class TransformerWrapper implements IClassTransformer + { + private final IClassTransformer parent; + + public TransformerWrapper() + { + try + { + this.parent = (IClassTransformer)this.getClass().getClassLoader().loadClass(getParentClass()).newInstance(); + } + catch(Exception e) + { + throw new RuntimeException(e); + } + } + + public byte[] transform(String name, String transformedName, byte[] basicClass) + { + try + { + return parent.transform(name, transformedName, basicClass); + } + catch(Throwable e) + { + throw new TransformerException("Exception in class transformer " + parent + " from coremod " + getCoreMod(), e); + } + } + + @Override + public String toString() + { + return "TransformerWrapper(" + getParentClass() + ", " + getCoreMod() + ")"; + } + + protected abstract String getParentClass(); + + protected abstract String getCoreMod(); + } + + static class TransformerException extends RuntimeException + { + public TransformerException(String message, Throwable cause) + { + super(message, cause); + } + } +} diff --git a/fml/src/main/java/net/minecraftforge/fml/common/asm/transformers/BlamingTransformer.java b/fml/src/main/java/net/minecraftforge/fml/common/asm/transformers/BlamingTransformer.java new file mode 100644 index 000000000..772bcaff4 --- /dev/null +++ b/fml/src/main/java/net/minecraftforge/fml/common/asm/transformers/BlamingTransformer.java @@ -0,0 +1,119 @@ +package net.minecraftforge.fml.common.asm.transformers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import net.minecraft.launchwrapper.IClassTransformer; +import net.minecraftforge.fml.common.FMLLog; + +import org.apache.commons.lang3.JavaVersion; +import org.apache.commons.lang3.SystemUtils; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; + +import com.google.common.collect.ImmutableSet; + +public class BlamingTransformer implements IClassTransformer +{ + private static final Map classMap = new HashMap(); + private static final Set naughtyMods = new HashSet(); + private static final Set naughtyClasses = new TreeSet(); + private static final Set orphanNaughtyClasses = new HashSet(); + + @Override + public byte[] transform(String name, String transformedName, byte[] bytes) + { + if (bytes == null) { return null; } + + ClassReader classReader = new ClassReader(bytes); + VersionVisitor visitor = new VersionVisitor(); + classReader.accept(visitor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); + return bytes; + } + + public static void blame(String modId, String cls) + { + naughtyClasses.add(cls); + naughtyMods.add(modId); + FMLLog.severe("Unsupported class format in mod %s: class %s", modId, cls); + } + + public static class VersionVisitor extends ClassVisitor + { + public VersionVisitor() + { + super(Opcodes.ASM5); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) + { + if( (version == Opcodes.V1_8 && !SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_1_8)) || + (version == Opcodes.V1_7 && !SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_1_7)) ) + { + if(classMap.containsKey(name)) blame(classMap.get(name), name); + else orphanNaughtyClasses.add(name); + } + } + } + + private static void checkPendingNaughty() + { + ImmutableSet.Builder toRemove = ImmutableSet.builder(); + for(String cls : orphanNaughtyClasses) + { + if(classMap.containsKey(cls)) + { + String modId = classMap.get(cls); + blame(modId, cls); + toRemove.add(cls); + } + } + orphanNaughtyClasses.removeAll(toRemove.build()); + } + + public static void addClasses(String modId, Set classList) + { + for(String cls : classList) + { + classMap.put(cls, modId); + } + checkPendingNaughty(); + } + + public static void onCrash(StringBuilder builder) + { + checkPendingNaughty(); + if(!naughtyClasses.isEmpty()) + { + builder.append("\n*** ATTENTION: detected classes with unsupported format ***\n"); + builder.append("*** DO NOT SUBMIT THIS CRASH REPORT TO FORGE ***\n\n"); + if(!naughtyMods.isEmpty()) + { + builder.append("Contact authors of the following mods: \n"); + for(String modId : naughtyMods) + { + builder.append(" ").append(modId).append("\n"); + } + } + if(!orphanNaughtyClasses.isEmpty()) + { + builder.append("Unidentified unsupported classes: \n"); + for(String cls : orphanNaughtyClasses) + { + builder.append(" ").append(cls).append("\n"); + } + } + builder.append('\n'); + } + } +} diff --git a/fml/src/main/java/net/minecraftforge/fml/relauncher/CoreModManager.java b/fml/src/main/java/net/minecraftforge/fml/relauncher/CoreModManager.java index 16947e64a..1d5f12880 100644 --- a/fml/src/main/java/net/minecraftforge/fml/relauncher/CoreModManager.java +++ b/fml/src/main/java/net/minecraftforge/fml/relauncher/CoreModManager.java @@ -25,6 +25,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarFile; @@ -32,6 +33,7 @@ import net.minecraft.launchwrapper.ITweaker; import net.minecraft.launchwrapper.Launch; import net.minecraft.launchwrapper.LaunchClassLoader; import net.minecraftforge.fml.common.FMLLog; +import net.minecraftforge.fml.common.asm.ASMTransformerWrapper; import net.minecraftforge.fml.common.asm.transformers.ModAccessTransformer; import net.minecraftforge.fml.common.launcher.FMLInjectionAndSortingTweaker; import net.minecraftforge.fml.common.launcher.FMLTweaker; @@ -50,6 +52,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.ObjectArrays; +import com.google.common.collect.Sets; import com.google.common.primitives.Ints; public class CoreModManager { @@ -58,12 +61,22 @@ public class CoreModManager { private static final Attributes.Name MODSIDE = new Attributes.Name("ModSide"); private static String[] rootPlugins = { "net.minecraftforge.fml.relauncher.FMLCorePlugin", "net.minecraftforge.classloading.FMLForgePlugin" }; private static List loadedCoremods = Lists.newArrayList(); + private static Map> transformers = Maps.newHashMap(); private static List loadPlugins; private static boolean deobfuscatedEnvironment; private static FMLTweaker tweaker; private static File mcDir; private static List reparsedCoremods = Lists.newArrayList(); private static List accessTransformers = Lists.newArrayList(); + private static Set rootNames = Sets.newHashSet(); + + static + { + for(String cls : rootPlugins) + { + rootNames.add(cls.substring(cls.lastIndexOf('.') + 1)); + } + } private static class FMLPluginWrapper implements ITweaker { public final String name; @@ -98,10 +111,19 @@ public class CoreModManager { public void injectIntoClassLoader(LaunchClassLoader classLoader) { FMLRelaunchLog.fine("Injecting coremod %s {%s} class transformers", name, coreModInstance.getClass().getName()); + List ts = Lists.newArrayList(); if (coreModInstance.getASMTransformerClass() != null) for (String transformer : coreModInstance.getASMTransformerClass()) { FMLRelaunchLog.finer("Registering transformer %s", transformer); - classLoader.registerTransformer(transformer); + classLoader.registerTransformer(ASMTransformerWrapper.getTransformerWrapper(classLoader, transformer, name)); + ts.add(transformer); + } + if(!rootNames.contains(name)) + { + String loc; + if(location == null) loc = "unknown"; + else loc = location.getName(); + transformers.put(name + " (" + loc + ")", ts); } FMLRelaunchLog.fine("Injection complete"); @@ -405,6 +427,11 @@ public class CoreModManager { return loadedCoremods; } + public static Map> getTransformers() + { + return transformers; + } + public static List getReparseableCoremods() { return reparsedCoremods; @@ -609,4 +636,17 @@ public class CoreModManager { { return accessTransformers; } + + public static void onCrash(StringBuilder builder) + { + if(!loadedCoremods.isEmpty() || !reparsedCoremods.isEmpty()) + { + builder.append("\nWARNING: coremods are present:\n"); + for(String coreMod : transformers.keySet()) + { + builder.append(" ").append(coreMod).append('\n'); + } + builder.append("Contact their authors BEFORE contacting forge\n\n"); + } + } } diff --git a/fml/src/main/java/net/minecraftforge/fml/relauncher/FMLCorePlugin.java b/fml/src/main/java/net/minecraftforge/fml/relauncher/FMLCorePlugin.java index f41e74ac1..0ffca569b 100644 --- a/fml/src/main/java/net/minecraftforge/fml/relauncher/FMLCorePlugin.java +++ b/fml/src/main/java/net/minecraftforge/fml/relauncher/FMLCorePlugin.java @@ -20,6 +20,7 @@ public class FMLCorePlugin implements IFMLLoadingPlugin public String[] getASMTransformerClass() { return new String[] { + "net.minecraftforge.fml.common.asm.transformers.BlamingTransformer", "net.minecraftforge.fml.common.asm.transformers.MarkerTransformer", "net.minecraftforge.fml.common.asm.transformers.SideTransformer", "net.minecraftforge.fml.common.asm.transformers.EventSubscriptionTransformer", diff --git a/fml/src/test/java/net/minecraftforge/fml/debug/FaultyCoreMod.java b/fml/src/test/java/net/minecraftforge/fml/debug/FaultyCoreMod.java new file mode 100644 index 000000000..f42d7ed68 --- /dev/null +++ b/fml/src/test/java/net/minecraftforge/fml/debug/FaultyCoreMod.java @@ -0,0 +1,29 @@ +package net.minecraftforge.fml.debug; + +import java.util.Map; + +import net.minecraft.launchwrapper.IClassTransformer; +import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; + +public class FaultyCoreMod implements IFMLLoadingPlugin { + public static boolean enabled = false; + + public String[] getASMTransformerClass() + { + return new String[] { FaultyTransformer.class.getName() }; + } + + public String getModContainerClass() { return null; } + public String getSetupClass() { return null; } + public void injectData(Map data) {} + public String getAccessTransformerClass() { return null; } + + public static class FaultyTransformer implements IClassTransformer { + + public byte[] transform(String name, String transformedName, byte[] basicClass) + { + if(enabled && name.equals("net.minecraft.client.gui.GuiMainMenu")) throw new RuntimeException("Faulty transformer test exception"); + return basicClass; + } + } +} diff --git a/fml/src/test/java/net/minecraftforge/fml/debug/Java8Debug.java b/fml/src/test/java/net/minecraftforge/fml/debug/Java8Debug.java new file mode 100644 index 000000000..a7dffa226 --- /dev/null +++ b/fml/src/test/java/net/minecraftforge/fml/debug/Java8Debug.java @@ -0,0 +1,26 @@ +/*package net.minecraftforge.fml.debug; + +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.Mod.EventHandler; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; + +@Mod(modid = Java8Debug.MODID, version = Java8Debug.VERSION) +public class Java8Debug implements ITest +{ + public static final String MODID = "Java8Debug"; + public static final String VERSION = "1.0"; + + @EventHandler + public void init(FMLInitializationEvent event) + { + System.out.println("Java8Debug mod loaded"); + } +} + +interface ITest +{ + default int test() + { + return 42; + } +}*/