/* * This file is part of Mixin, licensed under the MIT License (MIT). * * Copyright (c) SpongePowered * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.spongepowered.tools.agent; import java.lang.instrument.ClassDefinition; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.List; import org.spongepowered.asm.logging.Level; import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.ClassNode; import org.spongepowered.asm.mixin.transformer.IMixinTransformer; import org.spongepowered.asm.mixin.transformer.ext.IHotSwap; import org.spongepowered.asm.mixin.transformer.throwables.MixinReloadException; import org.spongepowered.asm.service.IMixinService; import org.spongepowered.asm.service.MixinService; import org.spongepowered.asm.service.ServiceNotAvailableError; import org.spongepowered.asm.transformers.MixinClassReader; import org.spongepowered.asm.util.asm.ASM; /** * An agent that re-transforms a mixin's target classes if the mixin has been * redefined. Basically this agent enables hot-swapping of mixins. */ public class MixinAgent implements IHotSwap { /** * Class file transformer that re-transforms mixin's target classes if the * mixin has been redefined. */ class Transformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain domain, byte[] classfileBuffer) throws IllegalClassFormatException { if (classBeingRedefined == null) { return null; } byte[] mixinBytecode = MixinAgent.classLoader.getFakeMixinBytecode(classBeingRedefined); if (mixinBytecode != null) { ClassNode classNode = new ClassNode(ASM.API_VERSION); ClassReader cr = new MixinClassReader(classfileBuffer, className); cr.accept(classNode, ClassReader.EXPAND_FRAMES); List targets = this.reloadMixin(className, classNode); if (targets == null || !this.reApplyMixins(targets)) { return MixinAgent.ERROR_BYTECODE; } return mixinBytecode; } try { MixinAgent.log(Level.INFO, "Redefining class {}", className); return MixinAgent.this.classTransformer.transformClassBytes(null, className.replace('/', '.'), classfileBuffer); } catch (Throwable th) { MixinAgent.log(Level.ERROR, "Error while re-transforming class {}", className, th); return MixinAgent.ERROR_BYTECODE; } } private List reloadMixin(String className, ClassNode classNode) { MixinAgent.log(Level.INFO, "Redefining mixin {}", className); try { return MixinAgent.this.classTransformer.reload(className.replace('/', '.'), classNode); } catch (MixinReloadException e) { MixinAgent.log(Level.ERROR, "Mixin {} cannot be reloaded, needs a restart to be applied: {} ", e.getMixinInfo(), e.getMessage()); } catch (Throwable th) { // catch everything as otherwise it is ignored MixinAgent.log(Level.ERROR, "Error while finding targets for mixin {}", className, th); } return null; } /** * Re-apply all mixins to the supplied list of target classes. * * @param targets Target classes to re-transform * @return true if all targets were transformed, false if transformation * failed */ private boolean reApplyMixins(List targets) { IMixinService service = MixinService.getService(); for (String target : targets) { String targetName = target.replace('/', '.'); MixinAgent.log(Level.DEBUG, "Re-transforming target class {}", target); try { Class targetClass = service.getClassProvider().findClass(targetName); byte[] targetBytecode = MixinAgent.classLoader.getOriginalTargetBytecode(targetName); if (targetBytecode == null) { MixinAgent.log(Level.ERROR, "Target class {} bytecode is not registered", targetName); return false; } targetBytecode = MixinAgent.this.classTransformer.transformClassBytes(null, targetName, targetBytecode); MixinAgent.instrumentation.redefineClasses(new ClassDefinition(targetClass, targetBytecode)); } catch (Throwable th) { MixinAgent.log(Level.ERROR, "Error while re-transforming target class {}", target, th); return false; } } return true; } } /** * Bytecode that signals an error. When returned from the class file * transformer this causes a class file format exception and indicates in * the ide that somethings went wrong. */ public static final byte[] ERROR_BYTECODE = new byte[]{1}; /** * Class loader used to load mixin classes */ static final MixinAgentClassLoader classLoader = new MixinAgentClassLoader(); /** * Instance used to register the transformer */ static Instrumentation instrumentation = null; /** * Instances of all agents */ private static List agents = new ArrayList(); /** * MixinTransformer instance to use to transform the mixin's target classes */ final IMixinTransformer classTransformer; /** * Constructs an agent from a class transformer in which it will use to * transform mixin's target class. * * @param classTransformer Class transformer that will transform a mixin's * target class */ public MixinAgent(IMixinTransformer classTransformer) { this.classTransformer = classTransformer; MixinAgent.agents.add(this); if (MixinAgent.instrumentation != null) { this.initTransformer(); } } private void initTransformer() { MixinAgent.instrumentation.addTransformer(new Transformer(), true); } @Override public void registerMixinClass(String name) { MixinAgent.classLoader.addMixinClass(name); } @Override public void registerTargetClass(String name, ClassNode classNode) { MixinAgent.classLoader.addTargetClass(name, classNode); } /** * Sets the instrumentation instance so that the mixin agents can redefine * mixins. * * @param instrumentation Instance to use to redefine the mixins */ public static void init(Instrumentation instrumentation) { MixinAgent.instrumentation = instrumentation; if (!MixinAgent.instrumentation.isRedefineClassesSupported()) { MixinAgent.log(Level.ERROR, "The instrumentation doesn't support re-definition of classes"); } for (MixinAgent agent : MixinAgent.agents) { agent.initTransformer(); } } /** * Initialize the java agent * *

This will be called automatically if the jar is in a -javaagent java * command line argument

* * @param arg Ignored * @param instrumentation Instance to use to transform the mixins */ public static void premain(String arg, Instrumentation instrumentation) { System.setProperty("mixin.hotSwap", "true"); MixinAgent.init(instrumentation); } /** * Initialize the java agent * *

This will be called automatically if the java agent is loaded after * JVVM startup

* * @param arg Ignored * @param instrumentation Instance to use to re-define the mixins */ public static void agentmain(String arg, Instrumentation instrumentation) { MixinAgent.init(instrumentation); } /** * Wrapper for logger since we can't access the log abstraction in premain * * @param level the logging level * @param message the message to log * @param params parameters to the message */ public static void log(Level level, String message, Object... params) { try { MixinService.getService().getLogger("mixin.agent").log(level, message, params); } catch (ServiceNotAvailableError err) { System.err.printf("MixinAgent: %s: %s", level.name(), String.format(message, params)); } } }