/**
* Analytica - beta version - Systems Monitoring Tool
*
* Copyright (C) 2013, KleeGroup, direction.technique@kleegroup.com (http://www.kleegroup.com)
* KleeGroup, Centre d'affaire la Boursidi�re - BP 159 - 92357 Le Plessis Robinson Cedex - France
*
* This program 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.
*
* This program 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 this program;
* if not, see <http://www.gnu.org/licenses>
*
* Linking this library statically or dynamically with other modules is making a combined work based on this library.
* Thus, the terms and conditions of the GNU General Public License cover the whole combination.
*
* As a special exception, the copyright holders of this library give you permission to link this library
* with independent modules to produce an executable, regardless of the license terms of these independent modules,
* and to copy and distribute the resulting executable under terms of your choice, provided that you also meet,
* for each linked independent module, the terms and conditions of the license of that module.
* An independent module is a module which is not derived from or based on this library.
* If you modify this library, you may extend this exception to your version of the library,
* but you are not obliged to do so.
* If you do not wish to do so, delete this exception statement from your version.
*/
package io.analytica.spies.imp.javassist;
import io.analytica.spies.imp.javassist.matcher.CompositeMatcher;
import io.analytica.spies.imp.javassist.matcher.CtBehaviorMatcher;
import io.analytica.spies.imp.javassist.matcher.InheritClassMatcher;
import io.analytica.spies.imp.javassist.matcher.Matcher;
import io.analytica.spies.imp.javassist.matcher.RegExpMatcher;
import io.analytica.spies.imp.javassist.matcher.StaticMatcher;
import io.analytica.spies.impl.JsonConfReader;
import java.io.InvalidClassException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.InvalidParameterException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.MethodInfo;
import javassist.runtime.Desc;
/**
* Impl�mentation de ClassFileTransformer pour instrumenter les m�thodes.
* Necessite d'�tre plac� dans un jar <b>avec</b> les class de Javassist, car il n'y a pas de gestion de classpath dans le javaagent
* @author npiedeloup
* @version $Id: MemoryLeakTransformer.java,v 1.1 2011/05/12 10:16:05 prahmoune Exp $
*/
final class AnalyticaSpyTransformer implements ClassFileTransformer {
private final AnalyticaSpyConf analyticaSpyConf;
private final Matcher<String> excludeMatcher;
private final Matcher<String> includeMatcher;
private final Map<Matcher<String>, AnalyticaSpyHookPoint> classNameHookPointMatchers = new LinkedHashMap<Matcher<String>, AnalyticaSpyHookPoint>();
private final Map<AnalyticaSpyHookPoint, Matcher<CtClass>> classHookPointMatchers = new LinkedHashMap<AnalyticaSpyHookPoint, Matcher<CtClass>>();
private final Map<AnalyticaSpyHookPoint, Matcher<CtBehavior>> methodHookPointMatchers = new LinkedHashMap<AnalyticaSpyHookPoint, Matcher<CtBehavior>>();
private final Map<String, CtClass> localVariables = new HashMap<String, CtClass>();
private final List<String> methodBefore;
private final List<String> methodAfter;
private final Map<CtClass, List<String>> methodCatchs = new HashMap<CtClass, List<String>>();
private final List<String> methodFinally;
private final List<ClassLoader> registeredClassLoader = new ArrayList<ClassLoader>();
private final ClassPool classPool = ClassPool.getDefault();
/**
* @param agentArgs parametres du javaagent de ligne de commande
*/
AnalyticaSpyTransformer(final String agentArgs) {
//Assertion.notEmpty(agentArgs); //pas Asssertion pour eviter une d�pendance
if (agentArgs == null || agentArgs.length() == 0) {
throw new IllegalArgumentException("Usage : -javaagent:analyticaAgent.jar=analyticaPlugs.json");
}
analyticaSpyConf = JsonConfReader.loadJsonConf(agentArgs, AnalyticaSpyConf.class);
System.out.println("Analytica start conf : " + analyticaSpyConf.toJson());
Container.initCollector(analyticaSpyConf);
excludeMatcher = buildStringMatchers(analyticaSpyConf.getFastExcludedPackages(), false);
includeMatcher = buildStringMatchers(analyticaSpyConf.getFastIncludedPackages(), true);
methodBefore = analyticaSpyConf.getMethodBefore();
methodAfter = analyticaSpyConf.getMethodAfter();
methodFinally = analyticaSpyConf.getMethodFinally();
for (final AnalyticaSpyHookPoint hookPoint : analyticaSpyConf.getHookPoints()) {
classNameHookPointMatchers.put(new RegExpMatcher(hookPoint.getClassName()), hookPoint);
if (hookPoint.getInherits() != null) {
classHookPointMatchers.put(hookPoint, new InheritClassMatcher(hookPoint.getInherits()));
}
methodHookPointMatchers.put(hookPoint, buildCtBehaviorMatchers(hookPoint.getMethods(), true));
}
Desc.useContextClassLoader = true;
}
/** {@inheritDoc} */
public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) throws IllegalClassFormatException {
try {
final String adaptedclassName = className.replace('/', '.');
final AnalyticaSpyHookPoint hookPoint = lookForHookPoint(loader, adaptedclassName);
if (hookPoint == null) {
return null;
}
//Sinon on instrument
final byte[] newClass = instrumentClass(loader, adaptedclassName, hookPoint);
return newClass;
} catch (final Throwable th) {
System.err.println("Could not instrument " + className + ", exception : " + th.getMessage());
return null;
}
}
/**
* @param loader Class loader
* @param className Nom de la classe
* @return si cette classe doit �tre instrument�e
*/
boolean shouldTransform(final ClassLoader loader, final String className) {
final String adaptedclassName = className.replace('/', '.');
return lookForHookPoint(loader, adaptedclassName) != null;
}
private void registerClassLoader(final ClassLoader loader) {
if (!registeredClassLoader.contains(loader)) {
classPool.appendClassPath(new javassist.LoaderClassPath(loader));
registeredClassLoader.add(loader);
}
}
private static Matcher<String> buildStringMatchers(final List<String> patterns, final boolean ifEmpty) {
if (patterns.isEmpty()) {
return new StaticMatcher<String>(ifEmpty);
}
final List<Matcher<String>> stringMatchers = new ArrayList<Matcher<String>>();
for (final String pattern : patterns) {
stringMatchers.add(new RegExpMatcher(pattern));
}
return new CompositeMatcher<String>(stringMatchers);
}
private static Matcher<CtBehavior> buildCtBehaviorMatchers(final List<String> patterns, final boolean ifEmpty) {
if (patterns.isEmpty()) {
return new StaticMatcher<CtBehavior>(ifEmpty);
}
final List<Matcher<CtBehavior>> ctBehaviorMatchers = new ArrayList<Matcher<CtBehavior>>();
for (final String pattern : patterns) {
ctBehaviorMatchers.add(new CtBehaviorMatcher(pattern));
}
return new CompositeMatcher<CtBehavior>(ctBehaviorMatchers);
}
private AnalyticaSpyHookPoint lookForHookPoint(final ClassLoader loader, final String adaptedclassName) {
//Test d'exclusions et d'inclusion rapide
if (excludeMatcher.isMatch(adaptedclassName) || !includeMatcher.isMatch(adaptedclassName)) {
// @see ClassFileTransformer : Returning null means that no transformation was done.
//System.out.println("Analytica exclude " + excludeMatcher.isMatch(adaptedclassName) + ", include " + !includeMatcher.isMatch(adaptedclassName) + " ");
return null;
}
//Test de d'inclusions par le nom de class (pour ne cr�er la CtClass qu'au besoin)
final List<AnalyticaSpyHookPoint> hookPoints = getHookPointByClassName(adaptedclassName);
if (hookPoints.isEmpty()) {
// @see ClassFileTransformer : Returning null means that no transformation was done.
//System.out.println("Analytica noHookPoint " + adaptedclassName + " ");
return null;
}
final CtClass ctClass = obtainCtClass(loader, adaptedclassName);
if (ctClass.isInterface()) {
return null; //pas d'instrumentation des interfaces
}
//Test de l'h�ritage si pr�sent
for (final AnalyticaSpyHookPoint hookPoint : hookPoints) {
final Matcher<CtClass> classMatcher = classHookPointMatchers.get(hookPoint);
if (classMatcher == null || classMatcher.isMatch(ctClass)) {
return hookPoint;
}
}
// @see ClassFileTransformer : Returning null means that no transformation was done.
return null;
}
private CtClass obtainCtClass(final ClassLoader loader, final String adaptedclassName) {
registerClassLoader(loader);
final CtClass cl;
try {
cl = classPool.get(adaptedclassName);
} catch (final NotFoundException e) {
System.err.println("Could not instrument " + adaptedclassName + ", exception : " + e.getMessage());
throw new RuntimeException(e);
}
return cl;
}
private List<AnalyticaSpyHookPoint> getHookPointByClassName(final String adaptedclassName) {
final List<AnalyticaSpyHookPoint> hookPoints = new ArrayList<AnalyticaSpyHookPoint>();
for (final Map.Entry<Matcher<String>, AnalyticaSpyHookPoint> entry : classNameHookPointMatchers.entrySet()) {
if (entry.getKey().isMatch(adaptedclassName)) {
hookPoints.add(entry.getValue());
}
}
return hookPoints;
}
private enum MethodType {
INIT, CLINIT, METHOD, // pr�sent dans MethodInfo
ABSTRACT, FINAL, NATIVE, PRIVATE, PROTECTED, PUBLIC, STATIC, SYNCHRONIZED // quelques javassist.Modifier
}
private byte[] instrumentClass(final ClassLoader loader, final String adaptedclassName, final AnalyticaSpyHookPoint hookPoint) {
System.out.println("Analytica instrument " + adaptedclassName + " with : " + hookPoint);
final CtClass ctClass = obtainCtClass(loader, adaptedclassName);
try {
final Matcher<CtBehavior> methodMatcher = methodHookPointMatchers.get(hookPoint);
try {
populateLocalVariables(analyticaSpyConf.getLocalVariables());
populateMethodCatchs(analyticaSpyConf.getMethodCatchs());
if (!ctClass.isInterface()) { // on ne peut pas instrumenter les interfaces
//final String agentManagerDef = "private final io.analytica.agent.AgentManager agentManager = Home.getContainer().getManager(io.analytica.agent.AgentManager.class);";
//final CtField agentManagerField = CtField.make(agentManagerDef, cl);
//cl.addField(agentManagerField);
final CtBehavior[] methods = ctClass.getDeclaredBehaviors();
for (final CtBehavior method : methods) {
if (method.isEmpty() == false && methodMatcher.isMatch(method)) {
if (isAttributsAccepted(hookPoint.getMethodTypes(), method)) {
try {
instrumentMethod(method, hookPoint);
} catch (final Exception e) {
System.err.println("Can't instrument " + adaptedclassName + "." + method.getName() + ", exception " + e.getClass().getName() + " : " + e.getMessage());
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
}
return ctClass.toBytecode();
}
throw new InvalidClassException("Can't instrument interfaces");
} catch (final Exception e) {
System.err.println("Can't instrument " + adaptedclassName + ", exception " + e.getClass().getName() + " : " + e.getMessage());
e.printStackTrace();
throw new RuntimeException(e);
}
} finally {
ctClass.detach();
}
}
private boolean isAttributsAccepted(final List<String> methodTypesStr, final CtBehavior method) {
final MethodInfo methodInfo = method.getMethodInfo();
final int modifiers = method.getModifiers();
boolean attributesAccepted = true;
for (final String attrStr : methodTypesStr) {
final boolean isNot = attrStr.startsWith("!");
final MethodType attr = MethodType.valueOf((isNot ? attrStr.substring(1) : attrStr).toUpperCase());
switch (attr) {
case INIT:
attributesAccepted = attributesAccepted && applyINot(isNot, methodInfo.isConstructor());
break;
case CLINIT:
attributesAccepted = attributesAccepted && applyINot(isNot, methodInfo.isStaticInitializer());
break;
case METHOD:
attributesAccepted = attributesAccepted && applyINot(isNot, methodInfo.isMethod());
break;
case ABSTRACT:
attributesAccepted = attributesAccepted && applyINot(isNot, Modifier.isAbstract(modifiers));
break;
case FINAL:
attributesAccepted = attributesAccepted && applyINot(isNot, Modifier.isFinal(modifiers));
break;
case NATIVE:
attributesAccepted = attributesAccepted && applyINot(isNot, Modifier.isNative(modifiers));
break;
case PRIVATE:
attributesAccepted = attributesAccepted && applyINot(isNot, Modifier.isPrivate(modifiers));
break;
case PROTECTED:
attributesAccepted = attributesAccepted && applyINot(isNot, Modifier.isProtected(modifiers));
break;
case PUBLIC:
attributesAccepted = attributesAccepted && applyINot(isNot, Modifier.isPublic(modifiers));
break;
case STATIC:
attributesAccepted = attributesAccepted && applyINot(isNot, Modifier.isStatic(modifiers));
break;
case SYNCHRONIZED:
attributesAccepted = attributesAccepted && applyINot(isNot, Modifier.isSynchronized(modifiers));
break;
default:
throw new InvalidParameterException("Type d'attributs non g�r� : " + attrStr);
}
if (!attributesAccepted) {
break; //inutil de continuer
}
}
return attributesAccepted;
}
private boolean applyINot(final boolean isNot, final boolean test) {
return isNot ? !test : test;
}
private void populateLocalVariables(final Map<String, String> localVariablesConf) throws NotFoundException {
for (final Map.Entry<String, String> entry : localVariablesConf.entrySet()) {
localVariables.put(entry.getKey(), classPool.get(entry.getValue()));
}
}
private void populateMethodCatchs(final Map<String, List<String>> methodCatchsConf) throws NotFoundException {
for (final Map.Entry<String, List<String>> entry : methodCatchsConf.entrySet()) {
methodCatchs.put(classPool.get(entry.getKey()), entry.getValue());
}
}
private void instrumentMethod(final CtBehavior method, final AnalyticaSpyHookPoint hookPoint) throws CannotCompileException {
//method.addLocalVariable("processType", classPool.get("String"));
//method.addLocalVariable("subTypes", classPool.get("java.lang.String[]"));
for (final Map.Entry<String, CtClass> entry : localVariables.entrySet()) {
method.addLocalVariable(entry.getKey(), entry.getValue());
//System.out.println("addLocalVariable " + entry.getKey() + " : " + entry.getValue().getName());
}
final String className = method.getDeclaringClass().getName();
final String methodName = method.getName();
final StringBuilder sbBefore = new StringBuilder();
appendPredefinedVariable(sbBefore, hookPoint, className, methodName);
for (final String line : methodBefore) {
if (line.length() != 0) {
sbBefore.append(line).append("\n");
}
}
if (sbBefore.length() > 0) {
method.insertBefore(replaceAlias(sbBefore.toString(), className, methodName));
}
//System.out.println("sbBefore " + sbBefore.toString());
final StringBuilder sbAfter = new StringBuilder();
for (final String line : methodAfter) {
if (line.length() != 0) {
sbAfter.append(line).append("\n");
}
}
if (sbAfter.length() > 0) {
method.insertAfter(replaceAlias(sbAfter.toString(), className, methodName), false);
}
//System.out.println("sbAfter " + sbAfter.toString());
for (final Map.Entry<CtClass, List<String>> entry : methodCatchs.entrySet()) {
final StringBuilder sbCatch = new StringBuilder();
for (final String line : entry.getValue()) {
if (line.length() != 0) {
sbCatch.append(line).append("\n");
}
}
method.addCatch(replaceAlias(sbCatch.toString(), className, methodName), entry.getKey());
//System.out.println("addCatch " + entry.getKey().getName() + " : " + entry.getValue());
}
final StringBuilder sbFinally = new StringBuilder();
for (final String line : methodFinally) {
if (line.length() != 0) {
sbFinally.append(line).append("\n");
}
}
if (sbFinally.length() > 0) {
method.insertAfter(replaceAlias(sbFinally.toString(), className, methodName), true);
}
//System.out.println("sbFinally " + sbFinally.toString());
System.out.println("Analytica instrument " + method.getLongName());
}
private void appendPredefinedVariable(final StringBuilder buffer, final AnalyticaSpyHookPoint hookPoint, final String className, final String methodName) {
buffer.append("final String processType = \"" + hookPoint.getProcessType() + "\";\n");
buffer.append("final String[] subTypes = new String[" + hookPoint.getSubTypes().size() + "];\n");
int i = 0;
for (final String subType : hookPoint.getSubTypes()) {
buffer.append("subTypes[").append(i).append("] = ");
final String updatedSubType = replaceAlias(subType, className, methodName);
if (subType.contains("$")) {
buffer.append(updatedSubType);
} else {
buffer.append("\"");
buffer.append(subType);
buffer.append("\"");
}
buffer.append(";\n");
i++;
}
}
private String replaceAlias(final String toReplace, final String className, final String methodName) {
String updated = toReplace.replaceAll("\\$methodName", "\"" + methodName + "\"");
updated = updated.replaceAll("\\$className", "\"" + className + "\"");
//updated = updated.replaceAll("\\$collector", "io.analytica.agent.impl.KProcessCollector processCollector = io.analytica.spies.imp.javassist.Container.getProcessCollector();");
updated = updated.replaceAll("\\$collector", "io.analytica.spies.imp.javassist.Container.getProcessCollector()");
return updated;
}
//
// private void addProfilingInformation(final CtClass clas, final CtMethod mold) throws NotFoundException, CannotCompileException {
// // get the method information (throws exception if method with
// // given name is not declared directly by this class, returns
// // arbitrary choice if more than one with the given name)
// final String mname = mold.getName();
// final String longName = mold.getLongName();
//
// // rename old method to synthetic name, then duplicate the
// // method with original name for use as interceptor
// final String nname = mname + "$impl";
//
// mold.setName(nname);
// final CtMethod mnew = CtNewMethod.copy(mold, mname, clas, null);
//
// // start the body text generation by saving the start time
// // to a local variable, then call the timed method; the
// // actual code generated needs to depend on whether the
// // timed method returns a value
// final String type = mold.getReturnType().getName();
// final StringBuffer body = new StringBuffer();
// body.append("{\n");
// body.append("int currentStackDepth = Thread.currentThread().getStackTrace().length;\n");
// body.append("StringBuffer buf = new StringBuffer();");
// body.append("for (int zs = 0; zs < currentStackDepth; zs++) {").append("buf.append(\" \");\n").append("}");
//
// body.append(" System.out.println(buf.toString() + \"-> Enter Method:" + longName + "\");\n");
// body.append(" long startTime = System.nanoTime();\n");
// body.append("try {\n");
//
// if (!"void".equals(type)) {
// body.append(type + " result = ");
// }
//
// body.append(nname + "($$);\n");
//
// if (!"void".equals(type)) {
// body.append("return result;\n");
// }
//
// body.append("} finally {");
// // finish body text generation with call to print the timing
// // information, and return saved value (if not void)
// body.append("long endTime = System.nanoTime();\n");
// body.append("long delta = endTime - startTime;\n");
//
// body.append("System.out.println(buf.toString() + \"<- Exit Method:" + longName + " completed in \" + delta + \" nano secs\");\n");
//
// body.append(" }\n");
// body.append("}");
//
// // replace the body of the interceptor method with generated
// // code block and add it to class
// mnew.setBody(body.toString());
// clas.addMethod(mnew);
// }
}