New class transformer cache
Hashes individual mixins/coremods and therefore needs no special invalidation logic
This commit is contained in:
parent
62eb72ea68
commit
41eef0b6ab
|
|
@ -1,51 +1,45 @@
|
|||
package cpw.mods.modlauncher;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.io.Files;
|
||||
import cpw.mods.modlauncher.api.ITransformer;
|
||||
import cpw.mods.modlauncher.api.ITransformerActivity;
|
||||
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
|
||||
import net.minecraftforge.coremod.transformer.CoreModBaseTransformer;
|
||||
import net.minecraftforge.fml.loading.FMLPaths;
|
||||
import net.minecraftforge.fml.loading.LoadingModList;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.embeddedt.modernfix.ModernFix;
|
||||
import org.embeddedt.modernfix.classloading.api.IHashableTransformer;
|
||||
import org.embeddedt.modernfix.classloading.hashers.CoreModTransformerHasher;
|
||||
import org.embeddedt.modernfix.classloading.hashers.MixinTransformerHasher;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.spongepowered.asm.launch.MixinLaunchPluginLegacy;
|
||||
|
||||
import javax.lang.model.SourceVersion;
|
||||
|
||||
public class ModernFixCachingClassTransformer extends ClassTransformer {
|
||||
private static final Logger LOGGER = LogManager.getLogger("ModernFixCachingTransformer");
|
||||
private Map<String, Optional<byte[]>> cache = new ConcurrentHashMap<>();
|
||||
private final static int QUEUE_SIZE = 512; // Config.recentCacheSize;
|
||||
Optional<Cache<String, byte[]>> recentCache = QUEUE_SIZE < 0 ? Optional.empty() :
|
||||
Optional.of(CacheBuilder.newBuilder().maximumSize(QUEUE_SIZE).build());
|
||||
|
||||
private static final boolean FORCE_REBUILD_CACHE = Boolean.parseBoolean(System.getProperty("coretweaks.transformerCache.full.forceRebuild", "false"));
|
||||
private static final File CLASS_CACHE_DAT = childFile(FMLPaths.GAMEDIR.get().resolve("modernfix").resolve("classTransformerV1.cache").toFile());
|
||||
private final LaunchPluginHandler pluginHandler;
|
||||
private final TransformStore transformStore;
|
||||
private final TransformerAuditTrail auditTrail;
|
||||
private final TransformingClassLoader transformingClassLoader;
|
||||
private final HashMap<String, List<ITransformer<?>>> transformersByClass;
|
||||
|
||||
public static final boolean DEBUG_PRINT = true;
|
||||
private static final int MAX_NUM_CLASSES = 10000;
|
||||
|
||||
private int lastSaveSize = 0;
|
||||
private BlockingQueue<String> dirtyClasses = new LinkedBlockingQueue<String>();
|
||||
private SaveThread saveThread = new SaveThread(this);
|
||||
|
||||
private static final File CLASS_CACHE_DAT = childFile(FMLPaths.GAMEDIR.get().resolve("modernfix").resolve("classTransformerFull.cache").toFile());
|
||||
private static final File CLASS_CACHE_DAT_ERRORED = childFile(FMLPaths.GAMEDIR.get().resolve("modernfix").resolve("classTransformerFull.cache.errored").toFile());
|
||||
private static final File CLASS_CACHE_DAT_TMP = childFile(FMLPaths.GAMEDIR.get().resolve("modernfix").resolve("classTransformerFull.cache~").toFile());
|
||||
private ConcurrentHashMap<String, Pair<List<byte[]>, byte[]>> transformationCache;
|
||||
|
||||
private static File childFile(File file) {
|
||||
file.getParentFile().mkdirs();
|
||||
|
|
@ -60,216 +54,143 @@ public class ModernFixCachingClassTransformer extends ClassTransformer {
|
|||
return SourceVersion.isName(className);
|
||||
}
|
||||
|
||||
static class SaveThread extends Thread {
|
||||
|
||||
private ModernFixCachingClassTransformer cacheTransformer;
|
||||
|
||||
private int saveInterval = 10000;
|
||||
|
||||
public SaveThread(ModernFixCachingClassTransformer ct) {
|
||||
this.cacheTransformer = ct;
|
||||
setName("CacheTransformer save thread");
|
||||
setDaemon(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while(true) {
|
||||
try {
|
||||
Thread.sleep(saveInterval);
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
cacheTransformer.doSave();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ModernFixCachingClassTransformer(TransformStore transformStore, LaunchPluginHandler pluginHandler, TransformingClassLoader transformingClassLoader, TransformerAuditTrail trail) {
|
||||
super(transformStore, pluginHandler, transformingClassLoader, trail);
|
||||
|
||||
if(FORCE_REBUILD_CACHE) {// || Persistence.modsChanged()) {
|
||||
clearCache(FORCE_REBUILD_CACHE ? "forceRebuild JVM flag was set." : "mods have changed.");
|
||||
} else {
|
||||
loadCache();
|
||||
}
|
||||
saveThread.start();
|
||||
}
|
||||
|
||||
private void clearCache(String reason) {
|
||||
LOGGER.info("Rebuilding class cache, because " + reason);
|
||||
CLASS_CACHE_DAT.delete();
|
||||
}
|
||||
|
||||
public void doSave() {
|
||||
saveCache();
|
||||
}
|
||||
|
||||
private void loadCache() {
|
||||
File inFile = CLASS_CACHE_DAT;
|
||||
|
||||
if(inFile.exists()) {
|
||||
LOGGER.info("Loading class cache.");
|
||||
cache.clear();
|
||||
|
||||
try (DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(inFile)))){
|
||||
try {
|
||||
while(true) { // EOFException should break the loop
|
||||
String className = in.readUTF();
|
||||
int classLength = in.readInt();
|
||||
byte[] classData = new byte[classLength];
|
||||
int bytesRead = in.read(classData, 0, classLength);
|
||||
|
||||
if(!isValidClassName(className)) {
|
||||
throw new RuntimeException("Invalid class name: " + className);
|
||||
} else if(bytesRead != classLength) {
|
||||
throw new RuntimeException("Length of " + className + " doesn't match advertised length of " + classLength);
|
||||
} else {
|
||||
cache.put(className, Optional.of(classData));
|
||||
|
||||
superDebug("Loaded " + className);
|
||||
}
|
||||
}
|
||||
} catch(EOFException eof) {}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("There was an error reading the transformer cache. A new one will be created. The previous one has been saved as " + CLASS_CACHE_DAT_ERRORED.getName() + " for inspection.");
|
||||
CLASS_CACHE_DAT.renameTo(CLASS_CACHE_DAT_ERRORED);
|
||||
e.printStackTrace();
|
||||
cache.clear();
|
||||
}
|
||||
LOGGER.info("Loaded " + cache.size() + " cached classes.");
|
||||
|
||||
lastSaveSize = cache.size();
|
||||
} else {
|
||||
LOGGER.info("Couldn't find class cache file");
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCacheFully() {
|
||||
File outFile = CLASS_CACHE_DAT;
|
||||
File outFileTmp = CLASS_CACHE_DAT_TMP;
|
||||
|
||||
LOGGER.info("Performing full save of class cache (size: " + cache.size() + ")");
|
||||
saveCacheChunk(cache.keySet(), outFileTmp, false);
|
||||
|
||||
this.transformStore = transformStore;
|
||||
this.pluginHandler = pluginHandler;
|
||||
this.transformingClassLoader = transformingClassLoader;
|
||||
this.auditTrail = trail;
|
||||
/* Build a lookup table of all transformers for a given class */
|
||||
this.transformersByClass = new HashMap<>();
|
||||
try {
|
||||
Files.move(outFileTmp, outFile);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to finish saving class cache");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCache() {
|
||||
if(dirtyClasses.isEmpty()) {
|
||||
return; // don't save if the cache hasn't changed
|
||||
}
|
||||
|
||||
File outFile = CLASS_CACHE_DAT;
|
||||
try {
|
||||
outFile.createNewFile();
|
||||
} catch (IOException e1) {
|
||||
// TODO Auto-generated catch block
|
||||
e1.printStackTrace();
|
||||
}
|
||||
|
||||
List<String> classesToSave = new ArrayList<String>();
|
||||
dirtyClasses.drainTo(classesToSave);
|
||||
|
||||
if(DEBUG_PRINT) {
|
||||
LOGGER.info("Saving class cache (size: " + lastSaveSize + " -> " + cache.size() + " | +" + classesToSave.size() + ")");
|
||||
}
|
||||
saveCacheChunk(classesToSave, outFile, true);
|
||||
|
||||
lastSaveSize += classesToSave.size();
|
||||
}
|
||||
|
||||
private void saveCacheChunk(Collection<String> classesToSave, File outFile, boolean append) {
|
||||
try(DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(outFile, append)))){
|
||||
for(String name : classesToSave) {
|
||||
Optional<byte[]> data = cache.get(name);
|
||||
if(data != null && data.isPresent()) {
|
||||
out.writeUTF(name);
|
||||
out.writeInt(data.get().length);
|
||||
out.write(data.get());
|
||||
Field transformersByTypeField = TransformStore.class.getDeclaredField("transformers");
|
||||
transformersByTypeField.setAccessible(true);
|
||||
Field transformersMapField = TransformList.class.getDeclaredField("transformers");
|
||||
transformersMapField.setAccessible(true);
|
||||
EnumMap<TransformTargetLabel.LabelType, TransformList<?>> transformersByType = (EnumMap<TransformTargetLabel.LabelType, TransformList<?>>)transformersByTypeField.get(this.transformStore);
|
||||
for(TransformList<?> transformList : transformersByType.values()) {
|
||||
Map<TransformTargetLabel, List<ITransformer<?>>> transformers = (Map<TransformTargetLabel, List<ITransformer<?>>>)transformersMapField.get(transformList);
|
||||
for(Map.Entry<TransformTargetLabel, List<ITransformer<?>>> entry : transformers.entrySet()) {
|
||||
String className = entry.getKey().getClassName().getClassName();
|
||||
List<ITransformer<?>> transformerList = this.transformersByClass.computeIfAbsent(className, k -> new ArrayList<>());
|
||||
transformerList.addAll(entry.getValue());
|
||||
}
|
||||
}
|
||||
if(DEBUG_PRINT) {
|
||||
LOGGER.info("Saved class cache");
|
||||
for(List<ITransformer<?>> transformerList : this.transformersByClass.values()) {
|
||||
transformerList.sort((t1, t2) -> Comparator.<String>naturalOrder().compare(StringUtils.join(t1.labels(), " "), StringUtils.join(t2.labels(), " ")));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Exception saving class cache");
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
} catch(ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
this.transformationCache = new ConcurrentHashMap<>();
|
||||
try(ObjectInputStream inStream = new ObjectInputStream(new FileInputStream(CLASS_CACHE_DAT))) {
|
||||
this.transformationCache = (ConcurrentHashMap<String, Pair<List<byte[]>,byte[]>>)inStream.readObject();
|
||||
int size = 0;
|
||||
/* Approximate the size in bytes */
|
||||
for(Map.Entry<String, Pair<List<byte[]>,byte[]>> entry : this.transformationCache.entrySet()) {
|
||||
size += entry.getKey().length();
|
||||
size += entry.getValue().getRight().length;
|
||||
for(byte[] hash : entry.getValue().getLeft()) {
|
||||
size += hash.length;
|
||||
}
|
||||
}
|
||||
LOGGER.info("Loaded transformer cache, contains " + this.transformationCache.size() + " classes and in-memory size is approximately " + FileUtils.byteCountToDisplaySize(size));
|
||||
} catch(IOException | ClassNotFoundException e) {
|
||||
LOGGER.error("An error occured while loading transform cache", e);
|
||||
}
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
LOGGER.info("Serializing transform cache to disk");
|
||||
try(ObjectOutputStream outStream = new ObjectOutputStream(new FileOutputStream(CLASS_CACHE_DAT))) {
|
||||
outStream.writeObject(transformationCache);
|
||||
} catch(IOException e) {
|
||||
LOGGER.error("An error occured while serializing transform cache", e);
|
||||
}
|
||||
}, "ModernFix transformer shutdown thread"));
|
||||
}
|
||||
|
||||
private String describeBytecode(byte[] basicClass) {
|
||||
return basicClass == null ? "null" : String.format("length: %d, hash: %x", basicClass.length, basicClass.hashCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the hashed list of transformers and use a cached version of the class if possible. This code needs
|
||||
* to be very fast as the entire point is to spend very little time doing transformation work that was done before.
|
||||
* @param inputClass The bytecode to be transformed
|
||||
* @param className Name of the class
|
||||
* @param reason Reason for the class being loaded
|
||||
* @return The transformed version of the class
|
||||
*/
|
||||
@Override
|
||||
public byte[] transform(byte[] basicClass, String transformedName, String reason) {
|
||||
public byte[] transform(byte[] inputClass, String className, String reason) {
|
||||
/* We only want to cache actual transformations */
|
||||
if(!ITransformerActivity.CLASSLOADING_REASON.equals(reason) || basicClass.length == 0) {
|
||||
return super.transform(basicClass, transformedName, reason);
|
||||
if(!ITransformerActivity.CLASSLOADING_REASON.equals(reason)) {
|
||||
return super.transform(inputClass, className, reason);
|
||||
}
|
||||
byte[] result = null;
|
||||
String name = transformedName;
|
||||
|
||||
try {
|
||||
boolean dontCache = false;
|
||||
/*
|
||||
for(String badPrefix : badClasses) {
|
||||
if(transformedName.startsWith(badPrefix)){
|
||||
dontCache = true;
|
||||
break;
|
||||
final String internalName = className.replace('.', '/');
|
||||
final Type classDesc = Type.getObjectType(internalName);
|
||||
final EnumMap<ILaunchPluginService.Phase, List<ILaunchPluginService>> launchPluginTransformerSet = pluginHandler.computeLaunchPluginTransformerSet(classDesc, false, reason, this.auditTrail);
|
||||
final boolean needsTransforming = transformStore.needsTransforming(internalName);
|
||||
if (!needsTransforming && launchPluginTransformerSet.isEmpty()) {
|
||||
return inputClass;
|
||||
}
|
||||
/* Now compute the hash list for the required transformers */
|
||||
ArrayList<byte[]> hashList = new ArrayList<>();
|
||||
for(List<ILaunchPluginService> pluginList : launchPluginTransformerSet.values()) {
|
||||
pluginList.sort((service1, service2) -> Comparator.<String>naturalOrder().compare(service1.name(), service2.name()));
|
||||
for(ILaunchPluginService service : pluginList) {
|
||||
byte[] hash = obtainHash(service, className);
|
||||
if(hash == null) {
|
||||
return super.transform(inputClass, className, reason);
|
||||
}
|
||||
hashList.add(hash);
|
||||
}
|
||||
}
|
||||
if(needsTransforming) {
|
||||
List<ITransformer<?>> transformers = this.transformersByClass.get(internalName);
|
||||
if(transformers != null) {
|
||||
for(ITransformer<?> transformer : transformers) {
|
||||
byte[] hash = obtainHash(transformer, className);
|
||||
if(hash == null) {
|
||||
return super.transform(inputClass, className, reason);
|
||||
}
|
||||
hashList.add(hash);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
if(cache.containsKey(transformedName) && !dontCache) {
|
||||
if(cache.get(transformedName).isPresent()) { // we still remember it
|
||||
result = cache.get(transformedName).get();
|
||||
|
||||
if(recentCache.isPresent()) {
|
||||
// classes are only loaded once, so no need to keep it around in RAM
|
||||
cache.put(transformedName, Optional.empty());
|
||||
|
||||
// but keep it around in case it's needed again by another transformer in the chain
|
||||
recentCache.get().put(transformedName, result);
|
||||
}
|
||||
} else if(recentCache.isPresent()){ // we have forgotten it, hopefully it's still around in the recent queue
|
||||
result = recentCache.get().getIfPresent(transformedName);
|
||||
if(result == null) {
|
||||
LOGGER.warn("Couldn't find " + transformedName + " in cache. Is recent queue too small? (" + QUEUE_SIZE + ")");
|
||||
}
|
||||
/* Check if the cache contains a transformed class matching these hashes */
|
||||
return transformationCache.compute(className, (name, oldPair) -> {
|
||||
boolean hashesMatch = true;
|
||||
if(oldPair == null || oldPair.getLeft().size() != hashList.size()) {
|
||||
hashesMatch = false;
|
||||
} else {
|
||||
for(int i = 0; i < oldPair.getLeft().size(); i++) {
|
||||
if(!Arrays.equals(oldPair.getLeft().get(i), hashList.get(i))) {
|
||||
hashesMatch = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(result == null){
|
||||
basicClass = super.transform(basicClass, transformedName, reason);
|
||||
|
||||
if(basicClass != null && !dontCache) {
|
||||
cache.put(transformedName, Optional.of(basicClass)); // then cache it
|
||||
dirtyClasses.add(transformedName);
|
||||
}
|
||||
result = basicClass;
|
||||
if(hashesMatch)
|
||||
return oldPair;
|
||||
else {
|
||||
if(oldPair != null) {
|
||||
LOGGER.warn("Hashes have changed, discarding cached version of " + name);
|
||||
}
|
||||
byte[] transformed = super.transform(inputClass, name, reason);
|
||||
return Pair.of(hashList, transformed);
|
||||
}
|
||||
if(result != null && recentCache.isPresent() && !dontCache) {
|
||||
recentCache.get().put(transformedName, result);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw e; // pass it to LaunchClassLoader, who will handle it
|
||||
} finally {
|
||||
//wrappedTransformers.alt = this;
|
||||
}
|
||||
return result;
|
||||
}).getRight();
|
||||
}
|
||||
|
||||
private void superDebug(String msg) {
|
||||
if(DEBUG_PRINT) {
|
||||
LOGGER.debug(msg);
|
||||
private static final byte[] FORGE_HASH = LoadingModList.get().getModFileById("forge").getMods().get(0).getVersion().toString().getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private byte[] obtainHash(Object o, String className) {
|
||||
if(o instanceof CoreModBaseTransformer) {
|
||||
return CoreModTransformerHasher.obtainHash((CoreModBaseTransformer<?>)o);
|
||||
} else if(o instanceof MixinLaunchPluginLegacy) {
|
||||
return MixinTransformerHasher.obtainHash((MixinLaunchPluginLegacy)o, className);
|
||||
} else if(o instanceof IHashableTransformer) {
|
||||
return ((IHashableTransformer)o).getHashForClass(className);
|
||||
} else if(o.getClass().getName().startsWith("net.minecraftforge.")) {
|
||||
return FORGE_HASH;
|
||||
} else {
|
||||
LOGGER.warn("No hash implementation found for: " + o.getClass().getName());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package org.embeddedt.modernfix.classloading.api;
|
||||
|
||||
public interface IHashableTransformer {
|
||||
/**
|
||||
* Called on an ILaunchPluginService or ITransformer to obtain a unique hash of the transformations that will be applied.
|
||||
* Used to invalidate the transformation cache when needed.
|
||||
* @param className Name of class being transformed
|
||||
* @return A unique hash of the transformations that will be applied
|
||||
*/
|
||||
byte[] getHashForClass(String className);
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package org.embeddedt.modernfix.classloading.hashers;
|
||||
|
||||
import net.minecraftforge.coremod.CoreMod;
|
||||
import net.minecraftforge.coremod.transformer.CoreModBaseTransformer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class CoreModTransformerHasher {
|
||||
private static final ConcurrentHashMap<CoreMod, byte[]> hashForCoremod;
|
||||
private static Field coremodField;
|
||||
private static ThreadLocal<MessageDigest> coremodHasher = ThreadLocal.withInitial(() -> {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-256");
|
||||
} catch(NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
static {
|
||||
hashForCoremod = new ConcurrentHashMap<>();
|
||||
try {
|
||||
coremodField = CoreModBaseTransformer.class.getDeclaredField("coreMod");
|
||||
coremodField.setAccessible(true);
|
||||
} catch(ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] hashCoreMod(CoreMod coreMod) {
|
||||
byte[] coreModContents;
|
||||
try {
|
||||
coreModContents = Files.readAllBytes(coreMod.getPath());
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
MessageDigest hasher = coremodHasher.get();
|
||||
byte[] hash = hasher.digest(coreModContents);
|
||||
hasher.reset();
|
||||
return hash;
|
||||
}
|
||||
|
||||
public static byte[] obtainHash(CoreModBaseTransformer<?> transformer) {
|
||||
CoreMod coremod;
|
||||
try {
|
||||
coremod = (CoreMod)coremodField.get(transformer);
|
||||
} catch(ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return hashForCoremod.computeIfAbsent(coremod, CoreModTransformerHasher::hashCoreMod);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package org.embeddedt.modernfix.classloading.hashers;
|
||||
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.io.Resources;
|
||||
import org.spongepowered.asm.launch.IClassProcessor;
|
||||
import org.spongepowered.asm.launch.MixinLaunchPluginLegacy;
|
||||
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
|
||||
import org.spongepowered.asm.service.MixinService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.URL;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
|
||||
public class MixinTransformerHasher {
|
||||
private static HashMap<String, byte[]> hashesByClass = null;
|
||||
private final static MessageDigest hasher;
|
||||
|
||||
static {
|
||||
try {
|
||||
hasher = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] obtainHash(MixinLaunchPluginLegacy plugin, String className) {
|
||||
synchronized (MixinTransformerHasher.class) {
|
||||
if(hashesByClass == null) {
|
||||
hashesByClass = new HashMap<>();
|
||||
HashMap<String, ArrayList<IMixinInfo>> mixinsByClass = new HashMap<>();
|
||||
try {
|
||||
Field processorsField = MixinLaunchPluginLegacy.class.getDeclaredField("processors");
|
||||
processorsField.setAccessible(true);
|
||||
List<IClassProcessor> processors = (List<IClassProcessor>)processorsField.get(plugin);
|
||||
Object transformHandler = null;
|
||||
for(IClassProcessor processor : processors) {
|
||||
if(processor.getClass().getName().equals("org.spongepowered.asm.service.modlauncher.MixinTransformationHandler")) {
|
||||
transformHandler = processor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(transformHandler == null)
|
||||
throw new IllegalStateException("Mixin transform handler not found");
|
||||
Field transformerField = transformHandler.getClass().getDeclaredField("transformer");
|
||||
transformerField.setAccessible(true);
|
||||
Object transformer = transformerField.get(transformHandler);
|
||||
Field processorField = transformer.getClass().getDeclaredField("processor");
|
||||
processorField.setAccessible(true);
|
||||
Object processor = processorField.get(transformer);
|
||||
Field configsField = processor.getClass().getDeclaredField("configs");
|
||||
configsField.setAccessible(true);
|
||||
List<?> configs = (List<?>)configsField.get(processor);
|
||||
Field mixinsField = Class.forName("org.spongepowered.asm.mixin.transformer.MixinConfig").getDeclaredField("mixins");
|
||||
mixinsField.setAccessible(true);
|
||||
/* getTargetClasses can't be used because it's package-private */
|
||||
Field classNamesField = Class.forName("org.spongepowered.asm.mixin.transformer.MixinInfo").getDeclaredField("targetClassNames");
|
||||
classNamesField.setAccessible(true);
|
||||
for(Object config : configs) {
|
||||
List<? extends IMixinInfo> mixins = (List<? extends IMixinInfo>)mixinsField.get(config);
|
||||
for(IMixinInfo mixin : mixins) {
|
||||
List<String> targetClassNames = (List<String>)classNamesField.get(mixin);
|
||||
for(String s : targetClassNames) {
|
||||
mixinsByClass.computeIfAbsent(s, k -> new ArrayList<>()).add(mixin);
|
||||
}
|
||||
}
|
||||
}
|
||||
for(ArrayList<IMixinInfo> infos : mixinsByClass.values()) {
|
||||
infos.sort((info1, info2) -> Comparator.<String>naturalOrder().compare(info1.getClassName(), info2.getClassName()));
|
||||
}
|
||||
/* Now go through each class name and hash it */
|
||||
for(Map.Entry<String, ArrayList<IMixinInfo>> mixinsForClass : mixinsByClass.entrySet()) {
|
||||
hasher.reset();
|
||||
for(IMixinInfo mixin : mixinsForClass.getValue()) {
|
||||
URL url = Thread.currentThread().getContextClassLoader().getResource(mixin.getClassName().replace('.', '/') + ".class");
|
||||
if(url == null)
|
||||
throw new IllegalStateException("Can't find " + mixin.getClassName());
|
||||
byte[] bytecode;
|
||||
try {
|
||||
bytecode = Resources.asByteSource(url).read();
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
hasher.update(bytecode);
|
||||
}
|
||||
hashesByClass.put(mixinsForClass.getKey().replace('/', '.'), hasher.digest());
|
||||
}
|
||||
} catch(ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return hashesByClass.getOrDefault(className, new byte[0]);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
|
|||
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
|
|
@ -26,9 +27,6 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
|
|||
private final Logger logger = LogManager.getLogger("ModernFix");
|
||||
public static ModernFixEarlyConfig config = null;
|
||||
|
||||
private static final boolean USE_TRANSFORMER_CACHE = false;
|
||||
private static final boolean USE_CLASS_LOCATION_CACHE = false;
|
||||
|
||||
public ModernFixMixinPlugin() {
|
||||
try {
|
||||
config = ModernFixEarlyConfig.load(new File("./config/modernfix-mixins.properties"));
|
||||
|
|
@ -53,7 +51,11 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
|
|||
TransformStore store = ObfuscationReflectionHelper.getPrivateValue(ClassTransformer.class, t, "transformers");
|
||||
LaunchPluginHandler pluginHandler = ObfuscationReflectionHelper.getPrivateValue(ClassTransformer.class, t, "pluginHandler");
|
||||
TransformerAuditTrail trail = ObfuscationReflectionHelper.getPrivateValue(ClassTransformer.class, t, "auditTrail");
|
||||
classTransformerField.set(loader, new ModernFixCachingClassTransformer(store, pluginHandler, (TransformingClassLoader)loader, trail));
|
||||
Class<?> newTransformerClass = Class.forName("cpw.mods.modlauncher.ModernFixCachingClassTransformer", true, ClassTransformer.class.getClassLoader());
|
||||
Constructor<?> constructor = newTransformerClass.getConstructor(TransformStore.class, LaunchPluginHandler.class, TransformingClassLoader.class, TransformerAuditTrail.class);
|
||||
ClassTransformer newTransformer = (ClassTransformer)constructor.newInstance(store, pluginHandler, loader, trail);
|
||||
classTransformerField.set(loader, newTransformer);
|
||||
logger.info("Successfully injected caching transformer");
|
||||
}
|
||||
if(isOptionEnabled("launch.class_search_cache.ModernFixResourceFinder")) {
|
||||
Field resourceFinderField = TransformingClassLoader.class.getDeclaredField("resourceFinder");
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user