/*
* Copyright (c) 2008-2013, Matthias Mann
* Copyright (C) 2014 Zhang,Yuexiang (xfeep)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Matthias Mann nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package nginx.clojure.wave;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import nginx.clojure.SuspendExecution;
import nginx.clojure.asm.ClassReader;
import nginx.clojure.asm.Type;
import nginx.clojure.logger.LoggerService;
import nginx.clojure.logger.TinyLogService;
import nginx.clojure.logger.TinyLogService.MsgType;
import nginx.clojure.wave.SuspendMethodVerifier.VerifyVarInfo;
/**
* <p>Collects information about classes and their suspendable methods.</p>
* <p>Provides access to configuration parameters and to logging</p>
*
* @author Matthias Mann
* @author Zhang,Yuexiang (xfeep)
*/
public class MethodDatabase implements LoggerService {
public static final String NGINX_CLOJURE_LOG_WAVE_LEVEL = "nginx.clojure.logger.wave.level";
public static final String SUSPEND_BLOCKING_STR = "blocking";
// public static final String SUSPEND_IGNORE_STR = "ignore";
public static final String SUSPEND_NONE_STR = "none";
public static final String SUSPEND_JUST_MARK_STR = "just_mark";
public static final String SUSPEND_NORMAL_STR = "normal";
public static final String SUSPEND_FAMILY_STR = "family";
public static final String SUSPEND_SKIP_STR = "skip";
public static final Integer SUSPEND_NONE = 0;
public static final Integer SUSPEND_BLOCKING = 1;
public static final Integer SUSPEND_FAMILY = 2;
/**
* Those methods with suspend type SUSPEND_JUST_MARK will not or can not be waved,
* but because they will call some SUSPEND_NORMAL methods, their caller should take care
* of this case generally will be waved.
*/
public static final Integer SUSPEND_JUST_MARK = 3;
public static final Integer SUSPEND_NORMAL = 4;
public static final Integer SUSPEND_SKIP = 5;
public static final String[] SUSPEND_TYPE_STRS = new String[] {
SUSPEND_NONE_STR,
SUSPEND_BLOCKING_STR,
SUSPEND_FAMILY_STR,
SUSPEND_JUST_MARK_STR,
SUSPEND_NORMAL_STR,
SUSPEND_SKIP_STR
};
public static final String EXCEPTION_NAME = Type.getInternalName(SuspendExecution.class);
public static final String EXCEPTION_DESC = Type.getDescriptor(SuspendExecution.class);
public static final Charset UTF_8 = Charset.forName("utf-8");
private final ClassLoader cl;
private final ConcurrentHashMap<String, ClassEntry> classes;
private final ConcurrentHashMap<String, LazyClassEntry> lazyClasses;
private final ArrayList<FuzzyLazyClassEntry> fuzzlyLazyClasses;
private final ArrayList<String> retransformedClasses = new ArrayList<String>();
private final ArrayList<File> workList;
private final ArrayList<String> filters;
private ArrayList<String> userDefinedWaveConfigFiles = new ArrayList<String>();
private Map<String, VerifyVarInfo[][]> verfiyMethodInfos;
private static LoggerService log;
private boolean verify;
private boolean dump;
private boolean allowMonitors;
private boolean allowBlocking;
private boolean allowOutofCoroutine = true;
private boolean hookDumpWaveCfg = false;
private boolean doNothing = false;
private boolean runTool = false;
private Pattern traceClassPattern = null;
private Pattern traceClassMethodPattern = null;
private String dumpDir;
public MethodDatabase(ClassLoader classloader) {
if(classloader == null) {
throw new NullPointerException("classloader");
}
this.cl = classloader;
classes = new ConcurrentHashMap<String, ClassEntry>();
lazyClasses = new ConcurrentHashMap<String, LazyClassEntry>();
fuzzlyLazyClasses = new ArrayList<MethodDatabase.FuzzyLazyClassEntry>();
workList = new ArrayList<File>();
filters = new ArrayList<String>();
getLog();
}
public ArrayList<String> getRetransformedClasses() {
return retransformedClasses;
}
public boolean isAllowMonitors() {
return allowMonitors;
}
public void setAllowMonitors(boolean allowMonitors) {
this.allowMonitors = allowMonitors;
}
public Pattern getTraceClassPattern() {
return traceClassPattern;
}
public void setTraceClassPattern(Pattern traceClassPattern) {
this.traceClassPattern = traceClassPattern;
}
public Pattern getTraceClassMethodPattern() {
return traceClassMethodPattern;
}
public void setTraceClassMethodPattern(Pattern traceClassMethodPattern) {
this.traceClassMethodPattern = traceClassMethodPattern;
}
public boolean isAllowBlocking() {
return allowBlocking;
}
public void setAllowBlocking(boolean allowBlocking) {
this.allowBlocking = allowBlocking;
}
public boolean isAllowOutofCoroutine() {
return allowOutofCoroutine;
}
public void setAllowOutofCoroutine(boolean allowOutofCoroutine) {
this.allowOutofCoroutine = allowOutofCoroutine;
}
public ConcurrentHashMap<String, ClassEntry> getClasses() {
return classes;
}
public Map<String, VerifyVarInfo[][]> getVerfiyMethodInfos() {
return verfiyMethodInfos;
}
public ConcurrentHashMap<String, LazyClassEntry> getLazyClasses() {
return lazyClasses;
}
public ArrayList<FuzzyLazyClassEntry> getFuzzlyLazyClasses() {
return fuzzlyLazyClasses;
}
public static LoggerService getLog() {
if (log == null) {
log = new TinyLogService(TinyLogService.getSystemPropertyOrDefaultLevel(NGINX_CLOJURE_LOG_WAVE_LEVEL, MsgType.error), System.err, System.err);
}
return log;
}
public static void setLog(LoggerService log) {
MethodDatabase.log = log;
}
public void setRunTool(boolean runTool) {
this.runTool = runTool;
}
public boolean isRunTool() {
return runTool;
}
public void setDoNothing(boolean doNothing) {
this.doNothing = doNothing;
}
public boolean isDoNothing() {
return doNothing;
}
public boolean isDump() {
return dump;
}
public void setDump(boolean dump) {
this.dump = dump;
}
public String getDumpDir() {
return dumpDir;
}
public void setDumpDir(String dumpDir) {
this.dumpDir = dumpDir;
}
public boolean isHookDumpWaveCfg() {
return hookDumpWaveCfg;
}
public void setHookDumpWaveCfg(boolean hookDumpWaveCfg) {
this.hookDumpWaveCfg = hookDumpWaveCfg;
}
public void setVerify(boolean verify) {
this.verify = verify;
if (verify && verfiyMethodInfos == null) {
verfiyMethodInfos = new ConcurrentHashMap<String, VerifyVarInfo[][]>();
}
}
public boolean isVerify() {
return verify;
}
public boolean isDebug() {
return log.isDebugEnabled();
}
public void setDebug(boolean debug) {
if (log instanceof TinyLogService) {
TinyLogService tlog = (TinyLogService) log;
tlog.setLevel(MsgType.debug);
}
}
public void error(String msg, Exception ex) {
if(log != null) {
log.error(msg, ex);
}
}
public void checkClass(File f) {
try {
FileInputStream fis = new FileInputStream(f);
CheckInstrumentationVisitor civ = checkFileAndClose(fis, f.getPath());
if(civ != null) {
recordSuspendableMethods(civ.getName(), civ.getClassEntry());
if(civ.needsInstrumentation()) {
if(civ.isAlreadyInstrumented()) {
info("Found instrumented class: %s", f.getPath());
} else {
info("Found class: %s", f.getPath());
workList.add(f);
}
}
}
} catch (UnableToInstrumentException ex) {
throw ex;
} catch (Exception ex) {
error(f.getPath(), ex);
}
}
public ArrayList<String> getFilters() {
return filters;
}
protected Integer checkMethodFamilySuspendType(ClassEntry ce, String fullname) {
Integer st = ce.check(fullname);
Integer fst = st;
if (st == null || st == SUSPEND_NONE) {
if (ce.getSuperName() != null) {
ClassEntry sce = classes.get(ce.getSuperName());
if (sce != null) {
st = checkMethodFamilySuspendType(sce, fullname);
}
if (st != null && (fst == null || fst == SUSPEND_NONE)) {
fst = st;
}
}
if ((st == null || st == SUSPEND_NONE) && ce.interfaces != null) {
for (String itf : ce.interfaces) {
ClassEntry sce = classes.get(itf);
if (sce != null) {
st = checkMethodFamilySuspendType(sce, fullname);
if (st != null && (fst == null || fst == SUSPEND_NONE)) {
fst = st;
}
}
}
}
}
return st == null ? fst : st;
}
public final Integer checkMethodSuspendType(String className, String method, boolean searchSuperClass) {
return checkMethodSuspendType(className, method, searchSuperClass, true);
}
public final Integer checkMethodSuspendType(String className, String method, boolean searchSuperClass, boolean returnDefault) {
if(method.charAt(0) == '<' && method.charAt(1) == 'c') {
return SUSPEND_NONE; // special methods are never suspendable
}
// if(isJavaCore(className)) {
// return SUSPEND_NONE;
// }
// String fullname = ClassEntry.key(methodName, methodDesc);
ClassEntry ce = MethodDatabaseUtil.buildClassEntryFamily(this, className);
if (ce == null) {
if (method.charAt(0) == '<') {
warn("contructor's class not found - assuming not suspendable: %s#%s", className, method);
return SUSPEND_NONE;
}
warn("not found class - assuming suspendable: %s#%s", className, method);
return SUSPEND_NORMAL;
}
Integer st = null;
if (searchSuperClass) {
st = checkMethodFamilySuspendType(ce, method);
}else {
st = ce.check(method);
}
if (st == null && returnDefault) {
if(method.charAt(0) == '<') {
return SUSPEND_NONE;
}
warn("Method not found in class - assuming suspendable: %s#%s", className, method);
st = SUSPEND_NORMAL;
}
return st;
}
public void recordSuspendableMethods(String className, ClassEntry entry) {
ClassEntry oldEntry;
synchronized(this) {
oldEntry = classes.put(className, entry);
}
if (log.isDebugEnabled()) {
if(oldEntry != null) {
if(!oldEntry.equals(entry)) {
warn("Duplicate class entries with different data for class: %s", className);
}
}
}
}
private boolean typeImplements(String type, ClassEntry ce, String itf) {
while (!"java/lang/Object".equals(type)) {
String[] itfs = ce.getInterfaces();
for (int i = 0; i < itfs.length; ++i) {
if (itfs[i].equals(itf)) {
return true;
}
}
for (int i = 0; i < itfs.length; ++i) {
if (typeImplements(itfs[i], MethodDatabaseUtil.buildClassEntryFamily(this, itfs[i]), itf)) {
return true;
}
}
type = ce.getSuperName();
ce = MethodDatabaseUtil.buildClassEntryFamily(this, type);
}
return false;
}
public String getCommonSuperClass(String classA, String classB) {
ClassEntry ace = MethodDatabaseUtil.buildClassEntryFamily(this, classA);
ClassEntry bce = MethodDatabaseUtil.buildClassEntryFamily(this, classB);
if (ace == null || bce == null) {
return "java/lang/Object";
}
if (ace.isInterface && bce.isInterface) {
if (typeImplements(classA, ace, classB)) {
return classB;
}
if (typeImplements(classB, bce, classA)) {
return classA;
}
return "java/lang/Object";
}
if (ace.isInterface) {
if (typeImplements(classB, bce, classA)) {
return classA;
} else {
return "java/lang/Object";
}
}
if (bce.isInterface) {
if (typeImplements(classA, ace, classB)) {
return classB;
} else {
return "java/lang/Object";
}
}
ArrayList<String> listA = getSuperClasses(classA);
ArrayList<String> listB = getSuperClasses(classB);
if(listA == null || listB == null) {
return null;
}
int idx = 0;
int num = Math.min(listA.size(), listB.size());
for(; idx<num ; idx++) {
String superClassA = listA.get(idx);
String superClassB = listB.get(idx);
if(!superClassA.equals(superClassB)) {
break;
}
}
if(idx > 0) {
return listA.get(idx-1);
}
return null;
}
public void debug(Object message) {
log.debug(message);
}
public void debug(Object message, Throwable t) {
log.debug(message, t);
}
public void debug(String format, Object... objects) {
log.debug(format, objects);
}
public void error(Object message) {
log.error(message);
}
public void error(Object message, Throwable t) {
log.error(message, t);
}
public void error(String format, Object... objects) {
log.error(format, objects);
}
public void fatal(Object message) {
log.fatal(message);
}
public void fatal(Object message, Throwable t) {
log.fatal(message, t);
}
public void fatal(String format, Object... objects) {
log.fatal(format, objects);
}
public void info(Object message) {
log.info(message);
}
public void info(Object message, Throwable t) {
log.info(message, t);
}
public void info(String format, Object... objects) {
log.info(format, objects);
}
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
public boolean isErrorEnabled() {
return log.isErrorEnabled();
}
public boolean isFatalEnabled() {
return log.isFatalEnabled();
}
public boolean isInfoEnabled() {
return log.isInfoEnabled();
}
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
public boolean isWarnEnabled() {
return log.isWarnEnabled();
}
public void trace(Object message) {
log.trace(message);
}
public void trace(Object message, Throwable t) {
log.trace(message, t);
}
public void warn(Object message) {
log.warn(message);
}
public void warn(Object message, Throwable t) {
log.warn(message, t);
}
public void trace(String format, Object... objects) {
log.trace(format, objects);
}
@Override
public void warn(String format, Object... objects) {
log.warn(format, objects);
}
public boolean isException(String className) {
for(;;) {
if("java/lang/Throwable".equals(className)) {
return true;
}
if("java/lang/Object".equals(className)) {
return false;
}
String superClass = getDirectSuperClass(className);
if(superClass == null) {
warn("Can't determine super class of %s", className);
return false;
}
className = superClass;
}
}
public ArrayList<File> getWorkList() {
return workList;
}
/**
* <p>Overwrite this function if Coroutines is used in a transformation chain.</p>
* <p>This method must create a new CheckInstrumentationVisitor and visit the
* specified class with it.</p>
* @param className the class the needs to be analysed
* @return a new CheckInstrumentationVisitor that has visited the specified
* class or null if the class was not found
*/
public CheckInstrumentationVisitor checkClass(String className) {
InputStream is = cl.getResourceAsStream(className + ".class");
if(is != null) {
return checkFileAndClose(is, className);
}
return null;
}
public ClassLoader getClassLoader() {
return cl;
}
public CheckInstrumentationVisitor checkClass(ClassReader r) {
try {
CheckInstrumentationVisitor civ = new CheckInstrumentationVisitor();
r.accept(civ, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES
| ClassReader.SKIP_CODE);
return civ;
} catch (UnableToInstrumentException ex) {
throw ex;
} catch (Exception ex) {
error(r.getClassName(), ex);
}
return null;
}
public CheckInstrumentationVisitor checkFileAndClose(InputStream is, String name) {
try {
try {
ClassReader r = new ClassReader(is);
CheckInstrumentationVisitor civ = new CheckInstrumentationVisitor();
r.accept(civ, ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES|ClassReader.SKIP_CODE);
return civ;
} finally {
is.close();
}
} catch (UnableToInstrumentException ex) {
throw ex;
} catch (Exception ex) {
error(name, ex);
}
return null;
}
public boolean isInterface(String className) {
ClassEntry ce = MethodDatabaseUtil.buildClassEntryFamily(this, className);
return ce != null && ce.isInterface;
}
private ArrayList<String> getSuperClasses(String className) {
ArrayList<String> result = new ArrayList<String>();
for(;;) {
result.add(0, className);
if("java/lang/Object".equals(className)) {
return result;
}
String superClass = getDirectSuperClass(className);
if(superClass == null) {
return result;
}
className = superClass;
}
}
protected String getDirectSuperClass(String className) {
ClassEntry ce = MethodDatabaseUtil.buildClassEntryFamily(this, className);
return ce == null ? "java/lang/Object" : ce.superName;
}
public boolean shouldIgnore(String className) {
for (String f : filters) {
if (className.startsWith(f)) {
return true;
}
}
return false;
}
public boolean meetTraceTargetClass(String clz) {
return traceClassPattern != null && traceClassPattern.matcher(clz).find();
}
public boolean meetTraceTargetClassMethod(String clz, String method) {
return traceClassMethodPattern != null && traceClassMethodPattern.matcher(clz + "." + method).find();
}
public boolean meetTraceTargetClassMethod(String clzAndmethod) {
return traceClassMethodPattern != null && traceClassMethodPattern.matcher(clzAndmethod).find();
}
public static boolean isJavaCore(String className) {
return className.startsWith("java/") || className.startsWith("javax/") ||
className.startsWith("sun/") || className.startsWith("com/sun/");
}
public static String[] toStringArray(List<?> l) {
if(l.isEmpty()) {
return null;
}
return l.toArray(new String[l.size()]);
}
public ArrayList<String> getUserDefinedWaveConfigFiles() {
return userDefinedWaveConfigFiles;
}
private static final ClassEntry CLASS_NOT_FOUND = new ClassEntry("<class not found>", new String[0], false);
public static final class LazyClassEntry {
private final LinkedHashMap<String, Integer> methods = new LinkedHashMap<String, Integer>();
private final String resource;
public LazyClassEntry(String resource) {
this.resource = resource;
}
public final LinkedHashMap<String, Integer> getMethods() {
return methods;
}
public final String getResource() {
return resource;
}
}
public static final class FuzzyLazyClassEntry {
private final LinkedHashMap<String, Integer> methods = new LinkedHashMap<String, Integer>();
private final String resource;
private final Pattern pattern;
public FuzzyLazyClassEntry(Pattern pattern, String resource) {
this.pattern = pattern;
this.resource = resource;
}
public final LinkedHashMap<String, Integer> getMethods() {
return methods;
}
public final String getResource() {
return resource;
}
public final Pattern getPattern() {
return pattern;
}
}
public static final class ClassEntry {
private final ConcurrentHashMap<String, Integer> methods;
private final boolean isInterface;
private final String superName;
private final String[] interfaces;
private boolean alreadyInstrumented;
public ClassEntry(String superName, String[] interfaces, boolean isInterface) {
this.superName = superName;
this.interfaces = interfaces;
this.isInterface = isInterface;
this.methods = new ConcurrentHashMap<String, Integer>();
}
public boolean isInterface() {
return isInterface;
}
public String[] getInterfaces() {
return interfaces;
}
public ConcurrentHashMap<String, Integer> getMethods() {
return methods;
}
public String getSuperName() {
return superName;
}
public final void set(String name, String desc, Integer suspendable) {
String nameAndDesc = key(name, desc);
methods.put(nameAndDesc, suspendable);
}
public final void set(String nameAndDesc, Integer suspendable) {
methods.put(nameAndDesc, suspendable);
}
public final Integer get(String nameAndDesc) {
return methods.get(nameAndDesc);
}
public final Integer check(String name, String desc) {
return methods.get(key(name, desc));
}
public final Integer check(String fullname) {
return methods.get(fullname);
}
@Override
public int hashCode() {
return superName.hashCode() * 67 + methods.hashCode();
}
@Override
public boolean equals(Object obj) {
if(!(obj instanceof ClassEntry)) {
return false;
}
final ClassEntry other = (ClassEntry)obj;
return (superName == other.superName || superName != null && superName.equals(other.superName)) && methods.equals(other.methods);
}
public static String key(String methodName, String methodDesc) {
return methodName.concat(methodDesc);
}
public boolean isAlreadyInstrumented() {
return alreadyInstrumented;
}
public void setAlreadyInstrumented(boolean alreadyInstrumented) {
this.alreadyInstrumented = alreadyInstrumented;
}
}
}