/*
* Copyright (C) 2016 Baidu, Inc. All Rights Reserved.
*/
package dodola.anole.lib;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class IncrementalVisitor extends ClassVisitor {
/**
* Defines the output type from this visitor.
*/
public enum OutputType {
/**
* provide instrumented classes that can be hot swapped at runtime with an override class.
*/
INSTRUMENT,
/**
* provide override classes that be be used to hot swap an instrumented class.
*/
OVERRIDE
}
public static final String PACKAGE = "dodola/anole/runtime";
public static final String ABSTRACT_PATCHES_LOADER_IMPL =
PACKAGE + "/AbstractPatchesLoaderImpl";
public static final String APP_PATCHES_LOADER_IMPL = PACKAGE + "/AppPatchesLoaderImpl";
protected static final Type INSTANT_RELOAD_EXCEPTION =
Type.getType(PACKAGE + "/InstantReloadException");
protected static final Type RUNTIME_TYPE = Type.getType("L" + PACKAGE + "/AndroidInstantRuntime;");
public static final Type DISABLE_ANNOTATION_TYPE =
Type.getType("Lcom/android/tools/ir/api/DisableInstantRun;");
protected static final boolean TRACING_ENABLED = Boolean.getBoolean("FDR_TRACING");
public static final Type CHANGE_TYPE = Type.getType("L" + PACKAGE + "/IncrementalChange;");
protected String visitedClassName;
protected String visitedSuperName;
protected final ClassNode classNode;
protected final List<ClassNode> parentNodes;
/**
* Enumeration describing a method of field access rights.
*/
protected enum AccessRight {
PRIVATE, PACKAGE_PRIVATE, PROTECTED, PUBLIC;
static AccessRight fromNodeAccess(int nodeAccess) {
if ((nodeAccess & Opcodes.ACC_PRIVATE) != 0) return PRIVATE;
if ((nodeAccess & Opcodes.ACC_PROTECTED) != 0) return PROTECTED;
if ((nodeAccess & Opcodes.ACC_PUBLIC) != 0) return PUBLIC;
return PACKAGE_PRIVATE;
}
}
public IncrementalVisitor(
ClassNode classNode,
List<ClassNode> parentNodes,
ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
this.classNode = classNode;
this.parentNodes = parentNodes;
}
protected static String getRuntimeTypeName(Type type) {
return "L" + type.getInternalName() + ";";
}
FieldNode getFieldByName(String fieldName) {
FieldNode fieldNode = getFieldByNameInClass(fieldName, classNode);
Iterator<ClassNode> iterator = parentNodes.iterator();
while (fieldNode == null && iterator.hasNext()) {
ClassNode parentNode = iterator.next();
fieldNode = getFieldByNameInClass(fieldName, parentNode);
}
return fieldNode;
}
protected static FieldNode getFieldByNameInClass(
String fieldName, ClassNode classNode) {
//noinspection unchecked ASM api.
List<FieldNode> fields = classNode.fields;
for (FieldNode field : fields) {
if (field.name.equals(fieldName)) {
return field;
}
}
return null;
}
protected MethodNode getMethodByName(String methodName, String desc) {
MethodNode methodNode = getMethodByNameInClass(methodName, desc, classNode);
Iterator<ClassNode> iterator = parentNodes.iterator();
while (methodNode == null && iterator.hasNext()) {
ClassNode parentNode = iterator.next();
methodNode = getMethodByNameInClass(methodName, desc, parentNode);
}
return methodNode;
}
protected static MethodNode getMethodByNameInClass(String methodName, String desc, ClassNode classNode) {
//noinspection unchecked ASM API
List<MethodNode> methods = classNode.methods;
for (MethodNode method : methods) {
if (method.name.equals(methodName) && method.desc.equals(desc)) {
return method;
}
}
return null;
}
protected static void trace(GeneratorAdapter mv, String s) {
mv.push(s);
mv.invokeStatic(Type.getType(PACKAGE + ".AndroidInstantRuntime"),
Method.getMethod("void trace(String)"));
}
protected static void trace(GeneratorAdapter mv, String s1,
String s2) {
mv.push(s1);
mv.push(s2);
mv.invokeStatic(Type.getType(PACKAGE + ".AndroidInstantRuntime"),
Method.getMethod("void trace(String, String)"));
}
protected static void trace(GeneratorAdapter mv, String s1,
String s2, String s3) {
mv.push(s1);
mv.push(s2);
mv.push(s3);
mv.invokeStatic(Type.getType(PACKAGE + ".AndroidInstantRuntime"),
Method.getMethod("void trace(String, String, String)"));
}
protected static void trace(GeneratorAdapter mv, String s1,
String s2, String s3, String s4) {
mv.push(s1);
mv.push(s2);
mv.push(s3);
mv.push(s4);
mv.invokeStatic(Type.getType(PACKAGE + ".AndroidInstantRuntime"),
Method.getMethod("void trace(String, String, String, String)"));
}
protected static void trace(GeneratorAdapter mv, int argsNumber) {
StringBuilder methodSignature = new StringBuilder("void trace(String");
for (int i = 0; i < argsNumber - 1; i++) {
methodSignature.append(", String");
}
methodSignature.append(")");
mv.invokeStatic(Type.getType(PACKAGE + ".AndroidInstantRuntime"),
Method.getMethod(methodSignature.toString()));
}
/**
* Simple Builder interface for common methods between all byte code visitors.
*/
public interface VisitorBuilder {
IncrementalVisitor build(ClassNode classNode,
List<ClassNode> parentNodes, ClassVisitor classVisitor);
String getMangledRelativeClassFilePath(String originalClassFilePath);
OutputType getOutputType();
}
protected static void main(
String[] args,
VisitorBuilder visitorBuilder) throws IOException {
if (args.length != 3) {
throw new IllegalArgumentException("Needs to be given an input and output directory "
+ "and a classpath");
}
File srcLocation = new File(args[0]);
File baseInstrumentedCompileOutputFolder = new File(args[1]);
FileUtils.emptyFolder(baseInstrumentedCompileOutputFolder);
Iterable<String> classPathStrings = Splitter.on(File.pathSeparatorChar).split(args[2]);
List<URL> classPath = Lists.newArrayList();
for (String classPathString : classPathStrings) {
File path = new File(classPathString);
if (!path.exists()) {
throw new IllegalArgumentException(
String.format("Invalid class path element %s", classPathString));
}
classPath.add(path.toURI().toURL());
}
classPath.add(srcLocation.toURI().toURL());
URL[] classPathArray = Iterables.toArray(classPath, URL.class);
ClassLoader classesToInstrumentLoader = new URLClassLoader(classPathArray, null) {
@Override
public URL getResource(String name) {
// Never delegate to bootstrap classes.
return findResource(name);
}
};
ClassLoader originalThreadContextClassLoader = Thread.currentThread()
.getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(classesToInstrumentLoader);
instrumentClasses(srcLocation,
baseInstrumentedCompileOutputFolder, visitorBuilder);
} finally {
Thread.currentThread().setContextClassLoader(originalThreadContextClassLoader);
}
}
private static void instrumentClasses(
File rootLocation,
File outLocation,
VisitorBuilder visitorBuilder) throws IOException {
Iterable<File> files =
Files.fileTreeTraverser().preOrderTraversal(rootLocation).filter(Files.isFile());
for (File inputFile : files) {
instrumentClass(rootLocation, inputFile, outLocation, visitorBuilder);
}
}
/**
* Defines when a method access flags are compatible with InstantRun technology.
* <p>
* - If the method is a bridge method, we do not enable it for instantReload.
* it is most likely only calling a twin method (same name, same parameters).
* - if the method is abstract, we don't add a redirection.
*
* @param access the method access flags
* @return true if the method should be InstantRun enabled, false otherwise.
*/
protected static boolean isAccessCompatibleWithInstantRun(int access) {
return ((access & Opcodes.ACC_ABSTRACT) == 0) && ((access & Opcodes.ACC_BRIDGE) == 0);
}
// public static void instrumentJar(File jarFile,
// VisitorBuilder visitorBuilder) {
//
// File optJar = new File(jarFile.getParent(), jarFile.getName() + ".opt")
//
// JarFile file = null;
// try {
// file = new JarFile(jarFile);
//
// Enumeration enumeration = file.entries();
// JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar));
//
// while (enumeration.hasMoreElements()) {
// JarEntry jarEntry = (JarEntry) enumeration.nextElement();
// String entryName = jarEntry.getName();
// ZipEntry zipEntry = new ZipEntry(entryName);
// InputStream inputStream = file.getInputStream(jarEntry);
// jarOutputStream.putNextEntry(zipEntry);
//
// ClassReader classReader = new ClassReader(inputStream);
// // override the getCommonSuperClass to use the thread context class loader instead of
// // the system classloader. This is useful as ASM needs to load classes from the project
// // which the system classloader does not have visibility upon.
// // TODO: investigate if there is not a simpler way than overriding.
// ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES) {
// @Override
// protected String getCommonSuperClass(final String type1, final String type2) {
// Class<?> c, d;
// ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// try {
// c = Class.forName(type1.replace('/', '.'), false, classLoader);
// d = Class.forName(type2.replace('/', '.'), false, classLoader);
// } catch (Exception e) {
// throw new RuntimeException(e.toString());
// }
// if (c.isAssignableFrom(d)) {
// return type1;
// }
// if (d.isAssignableFrom(c)) {
// return type2;
// }
// if (c.isInterface() || d.isInterface()) {
// return "java/lang/Object";
// } else {
// do {
// c = c.getSuperclass();
// } while (!c.isAssignableFrom(d));
// return c.getName().replace('.', '/');
// }
// }
// };
//
// ClassNode classNode = new ClassNode();
// classReader.accept(classNode, ClassReader.EXPAND_FRAMES);
//
// // when dealing with interface, we just copy the inputFile over without any changes unless
// // this is a package private interface.
// AccessRight accessRight = AccessRight.fromNodeAccess(classNode.access);
//
//
//
//
//
//
//
// }
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
public static File instrumentClass(
File inputRootDirectory,
File inputFile,
File outputDirectory,
VisitorBuilder visitorBuilder) throws IOException {
byte[] classBytes;
String path = FileUtils.relativePath(inputFile, inputRootDirectory);
if (!inputFile.getPath().endsWith(SdkConstants.DOT_CLASS)) {
File outputFile = new File(outputDirectory, path);
Files.createParentDirs(outputFile);
Files.copy(inputFile, outputFile);
return outputFile;
}
classBytes = Files.toByteArray(inputFile);
ClassReader classReader = new ClassReader(classBytes);
// override the getCommonSuperClass to use the thread context class loader instead of
// the system classloader. This is useful as ASM needs to load classes from the project
// which the system classloader does not have visibility upon.
// TODO: investigate if there is not a simpler way than overriding.
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES) {
@Override
protected String getCommonSuperClass(final String type1, final String type2) {
Class<?> c, d;
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
c = Class.forName(type1.replace('/', '.'), false, classLoader);
d = Class.forName(type2.replace('/', '.'), false, classLoader);
} catch (Exception e) {
throw new RuntimeException(e.toString());
}
if (c.isAssignableFrom(d)) {
return type1;
}
if (d.isAssignableFrom(c)) {
return type2;
}
if (c.isInterface() || d.isInterface()) {
return "java/lang/Object";
} else {
do {
c = c.getSuperclass();
} while (!c.isAssignableFrom(d));
return c.getName().replace('.', '/');
}
}
};
ClassNode classNode = new ClassNode();
classReader.accept(classNode, ClassReader.EXPAND_FRAMES);
// when dealing with interface, we just copy the inputFile over without any changes unless
// this is a package private interface.
AccessRight accessRight = AccessRight.fromNodeAccess(classNode.access);
File outputFile = new File(outputDirectory, path);
if ((classNode.access & Opcodes.ACC_INTERFACE) != 0) {
if (visitorBuilder.getOutputType() == OutputType.INSTRUMENT) {
// don't change the name of interfaces.
Files.createParentDirs(outputFile);
if (accessRight == AccessRight.PACKAGE_PRIVATE) {
classNode.access = classNode.access | Opcodes.ACC_PUBLIC;
classNode.accept(classWriter);
Files.write(classWriter.toByteArray(), outputFile);
} else {
// just copy the input file over, no change.
Files.write(classBytes, outputFile);
}
return outputFile;
} else {
return null;
}
}
if (isPackageInstantRunDisabled(inputFile, classNode)) {
if (visitorBuilder.getOutputType() == OutputType.INSTRUMENT) {
Files.createParentDirs(outputFile);
Files.write(classBytes, outputFile);
return outputFile;
} else {
return null;
}
}
List<ClassNode> parentsNodes = parseParents(inputFile, classNode);
outputFile = new File(outputDirectory, visitorBuilder.getMangledRelativeClassFilePath(path));
Files.createParentDirs(outputFile);
IncrementalVisitor visitor = visitorBuilder.build(classNode, parentsNodes, classWriter);
classNode.accept(visitor);
Files.write(classWriter.toByteArray(), outputFile);
return outputFile;
}
private static File getBinaryFolder(File inputFile, ClassNode classNode) {
return new File(inputFile.getAbsolutePath().substring(0,
inputFile.getAbsolutePath().length() - (classNode.name.length() + ".class".length())));
}
private static List<ClassNode> parseParents(
File inputFile, ClassNode classNode) throws IOException {
File binaryFolder = getBinaryFolder(inputFile, classNode);
List<ClassNode> parentNodes = new ArrayList<ClassNode>();
String currentParentName = classNode.superName;
while (currentParentName != null) {
File parentFile = new File(binaryFolder, currentParentName + ".class");
if (parentFile.exists()) {
InputStream parentFileClassReader = new BufferedInputStream(new FileInputStream(parentFile));
ClassReader parentClassReader = new ClassReader(parentFileClassReader);
ClassNode parentNode = new ClassNode();
parentClassReader.accept(parentNode, ClassReader.EXPAND_FRAMES);
parentNodes.add(parentNode);
currentParentName = parentNode.superName;
} else {
// May need method information from outside of the current project. Thread local class reader
// should be the one
try {
ClassReader parentClassReader = new ClassReader(
Thread.currentThread().getContextClassLoader().getResourceAsStream(
currentParentName + ".class"));
ClassNode parentNode = new ClassNode();
parentClassReader.accept(parentNode, ClassReader.EXPAND_FRAMES);
parentNodes.add(parentNode);
currentParentName = parentNode.superName;
} catch (IOException e) {
// Could not locate parent class. This is as far as we can go locating parents.
currentParentName = null;
}
}
}
return parentNodes;
}
private static ClassNode parsePackageInfo(
File inputFile, ClassNode classNode) throws IOException {
File packageFolder = inputFile.getParentFile();
File packageInfoClass = new File(packageFolder, "package-info.class");
if (packageInfoClass.exists()) {
InputStream reader = new BufferedInputStream(new FileInputStream(packageInfoClass));
ClassReader classReader = new ClassReader(reader);
ClassNode packageInfo = new ClassNode();
classReader.accept(packageInfo, ClassReader.EXPAND_FRAMES);
return packageInfo;
}
return null;
}
private static boolean isPackageInstantRunDisabled(
File inputFile, ClassNode classNode) throws IOException {
ClassNode packageInfoClass = parsePackageInfo(inputFile, classNode);
if (packageInfoClass != null) {
//noinspection unchecked
List<AnnotationNode> annotations = packageInfoClass.invisibleAnnotations;
if (annotations == null) {
return false;
}
for (AnnotationNode annotation : annotations) {
if (annotation.desc.equals(DISABLE_ANNOTATION_TYPE.getDescriptor())) {
return true;
}
}
}
return false;
}
}