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;
|
package cpw.mods.modlauncher;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.*;
|
||||||
import java.io.BufferedOutputStream;
|
import java.lang.reflect.Array;
|
||||||
import java.io.DataInputStream;
|
import java.lang.reflect.Field;
|
||||||
import java.io.DataOutputStream;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.io.EOFException;
|
import java.util.*;
|
||||||
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.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
|
|
||||||
import com.google.common.cache.Cache;
|
import cpw.mods.modlauncher.api.ITransformer;
|
||||||
import com.google.common.cache.CacheBuilder;
|
|
||||||
import com.google.common.io.Files;
|
|
||||||
import cpw.mods.modlauncher.api.ITransformerActivity;
|
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.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.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
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;
|
import javax.lang.model.SourceVersion;
|
||||||
|
|
||||||
public class ModernFixCachingClassTransformer extends ClassTransformer {
|
public class ModernFixCachingClassTransformer extends ClassTransformer {
|
||||||
private static final Logger LOGGER = LogManager.getLogger("ModernFixCachingTransformer");
|
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 ConcurrentHashMap<String, Pair<List<byte[]>, byte[]>> transformationCache;
|
||||||
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 static File childFile(File file) {
|
private static File childFile(File file) {
|
||||||
file.getParentFile().mkdirs();
|
file.getParentFile().mkdirs();
|
||||||
|
|
@ -60,216 +54,143 @@ public class ModernFixCachingClassTransformer extends ClassTransformer {
|
||||||
return SourceVersion.isName(className);
|
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) {
|
public ModernFixCachingClassTransformer(TransformStore transformStore, LaunchPluginHandler pluginHandler, TransformingClassLoader transformingClassLoader, TransformerAuditTrail trail) {
|
||||||
super(transformStore, pluginHandler, transformingClassLoader, trail);
|
super(transformStore, pluginHandler, transformingClassLoader, trail);
|
||||||
|
this.transformStore = transformStore;
|
||||||
if(FORCE_REBUILD_CACHE) {// || Persistence.modsChanged()) {
|
this.pluginHandler = pluginHandler;
|
||||||
clearCache(FORCE_REBUILD_CACHE ? "forceRebuild JVM flag was set." : "mods have changed.");
|
this.transformingClassLoader = transformingClassLoader;
|
||||||
} else {
|
this.auditTrail = trail;
|
||||||
loadCache();
|
/* Build a lookup table of all transformers for a given class */
|
||||||
}
|
this.transformersByClass = new HashMap<>();
|
||||||
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);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.move(outFileTmp, outFile);
|
Field transformersByTypeField = TransformStore.class.getDeclaredField("transformers");
|
||||||
} catch (IOException e) {
|
transformersByTypeField.setAccessible(true);
|
||||||
LOGGER.error("Failed to finish saving class cache");
|
Field transformersMapField = TransformList.class.getDeclaredField("transformers");
|
||||||
e.printStackTrace();
|
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);
|
||||||
private void saveCache() {
|
for(Map.Entry<TransformTargetLabel, List<ITransformer<?>>> entry : transformers.entrySet()) {
|
||||||
if(dirtyClasses.isEmpty()) {
|
String className = entry.getKey().getClassName().getClassName();
|
||||||
return; // don't save if the cache hasn't changed
|
List<ITransformer<?>> transformerList = this.transformersByClass.computeIfAbsent(className, k -> new ArrayList<>());
|
||||||
}
|
transformerList.addAll(entry.getValue());
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(DEBUG_PRINT) {
|
for(List<ITransformer<?>> transformerList : this.transformersByClass.values()) {
|
||||||
LOGGER.info("Saved class cache");
|
transformerList.sort((t1, t2) -> Comparator.<String>naturalOrder().compare(StringUtils.join(t1.labels(), " "), StringUtils.join(t2.labels(), " ")));
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch(ReflectiveOperationException e) {
|
||||||
LOGGER.info("Exception saving class cache");
|
throw new RuntimeException(e);
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
|
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
|
@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 */
|
/* We only want to cache actual transformations */
|
||||||
if(!ITransformerActivity.CLASSLOADING_REASON.equals(reason) || basicClass.length == 0) {
|
if(!ITransformerActivity.CLASSLOADING_REASON.equals(reason)) {
|
||||||
return super.transform(basicClass, transformedName, reason);
|
return super.transform(inputClass, className, reason);
|
||||||
}
|
}
|
||||||
byte[] result = null;
|
final String internalName = className.replace('.', '/');
|
||||||
String name = transformedName;
|
final Type classDesc = Type.getObjectType(internalName);
|
||||||
|
final EnumMap<ILaunchPluginService.Phase, List<ILaunchPluginService>> launchPluginTransformerSet = pluginHandler.computeLaunchPluginTransformerSet(classDesc, false, reason, this.auditTrail);
|
||||||
try {
|
final boolean needsTransforming = transformStore.needsTransforming(internalName);
|
||||||
boolean dontCache = false;
|
if (!needsTransforming && launchPluginTransformerSet.isEmpty()) {
|
||||||
/*
|
return inputClass;
|
||||||
for(String badPrefix : badClasses) {
|
}
|
||||||
if(transformedName.startsWith(badPrefix)){
|
/* Now compute the hash list for the required transformers */
|
||||||
dontCache = true;
|
ArrayList<byte[]> hashList = new ArrayList<>();
|
||||||
break;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
}
|
||||||
|
/* Check if the cache contains a transformed class matching these hashes */
|
||||||
if(cache.containsKey(transformedName) && !dontCache) {
|
return transformationCache.compute(className, (name, oldPair) -> {
|
||||||
if(cache.get(transformedName).isPresent()) { // we still remember it
|
boolean hashesMatch = true;
|
||||||
result = cache.get(transformedName).get();
|
if(oldPair == null || oldPair.getLeft().size() != hashList.size()) {
|
||||||
|
hashesMatch = false;
|
||||||
if(recentCache.isPresent()) {
|
} else {
|
||||||
// classes are only loaded once, so no need to keep it around in RAM
|
for(int i = 0; i < oldPair.getLeft().size(); i++) {
|
||||||
cache.put(transformedName, Optional.empty());
|
if(!Arrays.equals(oldPair.getLeft().get(i), hashList.get(i))) {
|
||||||
|
hashesMatch = false;
|
||||||
// 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 + ")");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(result == null){
|
if(hashesMatch)
|
||||||
basicClass = super.transform(basicClass, transformedName, reason);
|
return oldPair;
|
||||||
|
else {
|
||||||
if(basicClass != null && !dontCache) {
|
if(oldPair != null) {
|
||||||
cache.put(transformedName, Optional.of(basicClass)); // then cache it
|
LOGGER.warn("Hashes have changed, discarding cached version of " + name);
|
||||||
dirtyClasses.add(transformedName);
|
}
|
||||||
}
|
byte[] transformed = super.transform(inputClass, name, reason);
|
||||||
result = basicClass;
|
return Pair.of(hashList, transformed);
|
||||||
}
|
}
|
||||||
if(result != null && recentCache.isPresent() && !dontCache) {
|
}).getRight();
|
||||||
recentCache.get().put(transformedName, result);
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
|
||||||
throw e; // pass it to LaunchClassLoader, who will handle it
|
|
||||||
} finally {
|
|
||||||
//wrappedTransformers.alt = this;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void superDebug(String msg) {
|
private static final byte[] FORGE_HASH = LoadingModList.get().getModFileById("forge").getMods().get(0).getVersion().toString().getBytes(StandardCharsets.UTF_8);
|
||||||
if(DEBUG_PRINT) {
|
|
||||||
LOGGER.debug(msg);
|
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 org.spongepowered.asm.mixin.extensibility.IMixinInfo;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
@ -26,9 +27,6 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
|
||||||
private final Logger logger = LogManager.getLogger("ModernFix");
|
private final Logger logger = LogManager.getLogger("ModernFix");
|
||||||
public static ModernFixEarlyConfig config = null;
|
public static ModernFixEarlyConfig config = null;
|
||||||
|
|
||||||
private static final boolean USE_TRANSFORMER_CACHE = false;
|
|
||||||
private static final boolean USE_CLASS_LOCATION_CACHE = false;
|
|
||||||
|
|
||||||
public ModernFixMixinPlugin() {
|
public ModernFixMixinPlugin() {
|
||||||
try {
|
try {
|
||||||
config = ModernFixEarlyConfig.load(new File("./config/modernfix-mixins.properties"));
|
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");
|
TransformStore store = ObfuscationReflectionHelper.getPrivateValue(ClassTransformer.class, t, "transformers");
|
||||||
LaunchPluginHandler pluginHandler = ObfuscationReflectionHelper.getPrivateValue(ClassTransformer.class, t, "pluginHandler");
|
LaunchPluginHandler pluginHandler = ObfuscationReflectionHelper.getPrivateValue(ClassTransformer.class, t, "pluginHandler");
|
||||||
TransformerAuditTrail trail = ObfuscationReflectionHelper.getPrivateValue(ClassTransformer.class, t, "auditTrail");
|
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")) {
|
if(isOptionEnabled("launch.class_search_cache.ModernFixResourceFinder")) {
|
||||||
Field resourceFinderField = TransformingClassLoader.class.getDeclaredField("resourceFinder");
|
Field resourceFinderField = TransformingClassLoader.class.getDeclaredField("resourceFinder");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user