/* * This file is part of aion-emu <aion-emu.com>. * * aion-emu is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * aion-emu is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with aion-emu. If not, see <http://www.gnu.org/licenses/>. */ package com.aionemu.commons.callbacks; import java.io.ByteArrayInputStream; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.lang.reflect.Modifier; import java.security.ProtectionDomain; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtField; import javassist.CtMethod; import javassist.LoaderClassPath; import javassist.NotFoundException; import javassist.bytecode.AnnotationsAttribute; import javassist.bytecode.annotation.Annotation; import javolution.util.FastMap; import org.apache.log4j.Logger; import com.aionemu.commons.utils.ExitCode; /** * This class is used as javaagent to do on-class-load transformations with objects whose methods are marked by * {@link com.aionemu.commons.callbacks.Enhancable} annotation.<br> * Code is inserted dynamicly before method call and after method call.<br> * For implementation docs please reffer to: http://www.csg.is.titech.ac.jp/~chiba/javassist/tutorial/tutorial2.html<br> * <br> * Usage: java -javaagent:lib/ae_commons.jar * * @author SoulKeeper */ public class JavaAgentEnhancer implements ClassFileTransformer { /** * Logger */ private static final Logger log = Logger.getLogger(JavaAgentEnhancer.class); /** * Field name for callbacks map */ public static final String FIELD_NAME_CALLBACKS = "$$$callbacks"; /** * Field name for synchronizer */ public static final String FIELD_NAME_CALLBACKS_LOCK = "$$$callbackLock"; /** * Premain method that registers this class as ClassFileTransformer * * @param args * arguments passed to javaagent, ignored * @param instrumentation * Instrumentation object */ public static void premain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new JavaAgentEnhancer(), true); } /** * This method analyzes class and adds callback support if needed. * * @param loader * ClassLoader of class * @param className * class name * @param classBeingRedefined * not used * @param protectionDomain * not used * @param classfileBuffer * basic class data */ @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { try { // no need to scan whole jvm boot classpath if(loader == null) { return null; } // classes from jvm ext dir, no need to modify if(loader.getClass().getName().equals("sun.misc.Launcher$ExtClassLoader")) { return null; } // actual class transformation return transformClass(loader, classfileBuffer); } catch(Exception e) { Error e1 = new Error("Can't transform class " + className, e); log.fatal(e1); // if it is a class from core (not a script) - terminate server // noinspection ConstantConditions if(loader.getClass().getName().equals("sun.misc.Launcher$AppClassLoader")) { Runtime.getRuntime().halt(ExitCode.CODE_ERROR); } throw e1; } } /** * Does actual transformation * * @param loader * class loader * @param clazzBytes * class bytecode * @return transformed class bytecode * @throws Exception * is something went wrong */ protected byte[] transformClass(ClassLoader loader, byte[] clazzBytes) throws Exception { ClassPool cp = new ClassPool(); cp.appendClassPath(new LoaderClassPath(loader)); CtClass clazz = cp.makeClass(new ByteArrayInputStream(clazzBytes)); Set<CtMethod> methdosToEnhance = new HashSet<CtMethod>(); for(CtMethod method : clazz.getMethods()) { if(!isEnhancable(method)) { continue; } methdosToEnhance.add(method); } if(!methdosToEnhance.isEmpty()) { CtClass eo = cp.get(EnhancedObject.class.getName()); for(CtClass i : clazz.getInterfaces()) { if(i.equals(eo)) { throw new RuntimeException("Class already implements EnhancedObject interface, WTF???"); } } writeEnhancedObjectImpl(clazz); for(CtMethod method : methdosToEnhance) { enhanceMethod(method); } return clazz.toBytecode(); } else { return null; } } /** * Resposible for method enhancing, writing service calls to method. * * @param method * Method that has to be edited * @throws CannotCompileException * if something went wrong * @throws NotFoundException * if something went wrong */ protected void enhanceMethod(CtMethod method) throws CannotCompileException, NotFoundException { ClassPool cp = method.getDeclaringClass().getClassPool(); method.addLocalVariable("___cbr", cp.get(CallbackResult.class.getName())); Annotation enhancable = null; for(Object o : method.getMethodInfo().getAttributes()) { if(o instanceof AnnotationsAttribute) { AnnotationsAttribute attribute = (AnnotationsAttribute) o; enhancable = attribute.getAnnotation(Enhancable.class.getName()); break; } } // noinspection ConstantConditions String listenerClassName = enhancable.getMemberValue("callback").toString(); listenerClassName = listenerClassName.substring(1, listenerClassName.length() - 7); int paramLength = method.getParameterTypes().length; method.insertBefore(writeBeforeMethod(method, paramLength, listenerClassName)); method.insertAfter(writeAfterMethod(method, paramLength, listenerClassName)); } /** * Code that is added in the begining of the method * * @param method * method that should be edited * @param paramLength * Lenght of methods parameters * @param listenerClassName * Listener class that is used for method * @return code that will be inserted before method * @throws NotFoundException * if something went wrong */ protected String writeBeforeMethod(CtMethod method, int paramLength, String listenerClassName) throws NotFoundException { StringBuilder sb = new StringBuilder(); sb.append('{'); sb.append(" ___cbr = "); sb.append(CallbackHelper.class.getName()).append(".beforeCall(("); sb.append(EnhancedObject.class.getName()); sb.append(")this, Class.forName(\""); sb.append(listenerClassName).append("\", true, getClass().getClassLoader()), "); if(paramLength > 0) { sb.append("new Object[]{"); for(int i = 1; i <= paramLength; i++) { sb.append("($w)$").append(i); if(i < paramLength) { sb.append(','); } } sb.append("}"); } else { sb.append("null"); } sb.append(");"); sb.append("if(___cbr.isBlockingCaller()){"); // Fake return due to javassist bug // $r is not available in "insertBefore" CtClass returnType = method.getReturnType(); if(returnType.equals(CtClass.voidType)) { sb.append("return"); } else if(returnType.equals(CtClass.booleanType)) { sb.append("return false"); } else if(returnType.equals(CtClass.charType)) { sb.append("return 'a'"); } else if(returnType.equals(CtClass.byteType) || returnType.equals(CtClass.shortType) || returnType.equals(CtClass.intType) || returnType.equals(CtClass.floatType) || returnType.equals(CtClass.longType) || returnType.equals(CtClass.longType)) { sb.append("return 0"); } sb.append(";}}"); return sb.toString(); } /** * Writes code that will be inserted after method * * @param method * method to edit * @param paramLength * lenght of method paramenters * @param listenerClassName * method listener * @return actual code that should be inserted * @throws NotFoundException * if something went wrong */ protected String writeAfterMethod(CtMethod method, int paramLength, String listenerClassName) throws NotFoundException { StringBuilder sb = new StringBuilder(); sb.append('{'); // workaround for javassist bug, $r is not available in "insertBefore" if(!method.getReturnType().equals(CtClass.voidType)) { sb.append("if(___cbr.isBlockingCaller()){"); sb.append("$_ = ($r)($w)___cbr.getResult();"); sb.append("}"); } sb.append("___cbr = ").append(CallbackHelper.class.getName()).append(".afterCall(("); sb.append(EnhancedObject.class.getName()).append(")this, Class.forName(\""); sb.append(listenerClassName).append("\", true, getClass().getClassLoader()), "); if(paramLength > 0) { sb.append("new Object[]{"); for(int i = 1; i <= paramLength; i++) { sb.append("($w)$").append(i); if(i < paramLength) { sb.append(','); } } sb.append("}"); } else { sb.append("null"); } sb.append(", ($w)$_);"); sb.append("if(___cbr.isBlockingCaller()){"); if(method.getReturnType().equals(CtClass.voidType)) { sb.append("return;"); } else { sb.append("return ($r)($w)___cbr.getResult();"); } sb.append("}"); sb.append("else {return $_;}"); sb.append("}"); return sb.toString(); } /** * Implements {@link EnhancedObject on class} * * @param clazz * class to edit * @throws NotFoundException * if something went wrong * @throws CannotCompileException * if something went wrong */ protected void writeEnhancedObjectImpl(CtClass clazz) throws NotFoundException, CannotCompileException { ClassPool cp = clazz.getClassPool(); clazz.addInterface(cp.get(EnhancedObject.class.getName())); writeEnhancedOBjectFields(clazz); writeEnhancedObjectMethods(clazz); } /** * Implements {@link EnhancedObject} fields * * @param clazz * Class to add fields * @throws CannotCompileException * if something went wrong * @throws NotFoundException * if something went wrong */ private void writeEnhancedOBjectFields(CtClass clazz) throws CannotCompileException, NotFoundException { ClassPool cp = clazz.getClassPool(); CtField cbField = new CtField(cp.get(List.class.getName()), FIELD_NAME_CALLBACKS, clazz); cbField.setModifiers(Modifier.PRIVATE); clazz.addField(cbField, CtField.Initializer.byExpr("new " + FastMap.class.getName() + "();")); CtField cblField = new CtField(cp.get(ReentrantLock.class.getName()), FIELD_NAME_CALLBACKS_LOCK, clazz); cblField.setModifiers(Modifier.PRIVATE); clazz.addField(cblField, CtField.Initializer.byExpr("new " + ReentrantLock.class.getName() + "();")); } /** * Implements {@link EnhancedObject methods} * * @param clazz * Class to add methods * @throws NotFoundException * if something went wrong * @throws CannotCompileException * if something went wrong */ private void writeEnhancedObjectMethods(CtClass clazz) throws NotFoundException, CannotCompileException { ClassPool cp = clazz.getClassPool(); CtClass callbackClass = cp.get(Callback.class.getName()); CtClass mapClass = cp.get(Map.class.getName()); CtClass reentrantReadWriteLockClass = cp.get(ReentrantLock.class.getName()); CtMethod method = new CtMethod(CtClass.voidType, "addCallback", new CtClass[] { callbackClass }, clazz); method.setModifiers(Modifier.PUBLIC); method.setBody("com.aionemu.commons.callbacks.CallbackHelper.addCallback($1, this);"); clazz.addMethod(method); method = new CtMethod(CtClass.voidType, "removeCallback", new CtClass[] { callbackClass }, clazz); method.setModifiers(Modifier.PUBLIC); method.setBody("com.aionemu.commons.callbacks.CallbackHelper.removeCallback($1, this);"); clazz.addMethod(method); method = new CtMethod(mapClass, "getCallbacks", new CtClass[] {}, clazz); method.setModifiers(Modifier.PUBLIC); method.setBody("return " + FIELD_NAME_CALLBACKS + ";"); clazz.addMethod(method); method = new CtMethod(reentrantReadWriteLockClass, "getCallbackLock", new CtClass[] {}, clazz); method.setModifiers(Modifier.PUBLIC); method.setBody("return " + FIELD_NAME_CALLBACKS_LOCK + ";"); clazz.addMethod(method); } /** * Checks if annotation is present on method * * @param method * Method to check * @param annotation * Annotation to look for * @return result */ protected boolean isAnnotationPresent(CtMethod method, Class<? extends java.lang.annotation.Annotation> annotation) { for(Object o : method.getMethodInfo().getAttributes()) { if(o instanceof AnnotationsAttribute) { AnnotationsAttribute attribute = (AnnotationsAttribute) o; if(attribute.getAnnotation(annotation.getName()) != null) { return true; } } } return false; } /** * Checks if method is enhancable. It should be marked with {@link com.aionemu.commons.callbacks.Enhancable} * annotation, be not native and not abstract * * @param method * method to check * @return check result */ protected boolean isEnhancable(CtMethod method) { int modifiers = method.getModifiers(); return !(Modifier.isAbstract(modifiers) || Modifier.isNative(modifiers)) && isAnnotationPresent(method, Enhancable.class); } }