debugger agent: implemented recursive async handling

This commit is contained in:
Egor.Ushakov
2017-09-26 12:08:30 +03:00
parent c6830aac66
commit f8a11aec74
3 changed files with 257 additions and 31 deletions
@@ -15,9 +15,10 @@
*/
package com.intellij.rt.debugger.agent;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import sun.misc.JavaLangAccess;
import sun.misc.SharedSecrets;
import java.util.*;
/**
* @author egor
@@ -25,22 +26,117 @@ import java.util.Map;
public class CaptureStorage {
private static final int MAX_STORED_STACKS = 1000;
public static final Map<Object, Exception> STORAGE = Collections.synchronizedMap(new LinkedHashMap<Object, Exception>() {
private static final Map<Object, CapturedStack> STORAGE = Collections.synchronizedMap(new LinkedHashMap<Object, CapturedStack>() {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_STORED_STACKS;
}
});
private static final ThreadLocal<Deque<InsertMatch>> CURRENT_STACKS = ThreadLocal.withInitial(LinkedList::new);
private static final JavaLangAccess ourJavaLangAccess = SharedSecrets.getJavaLangAccess();
@SuppressWarnings("unused")
public static void capture(Object key) {
System.out.println("capture: " + key);
Deque<InsertMatch> currentStacks = CURRENT_STACKS.get();
CapturedStack stack;
if (currentStacks.isEmpty()) {
stack = new CapturedStack(new Throwable());
}
else {
stack = new CapturedStack(new Throwable(), currentStacks.getLast());
System.out.println("capture: insertion added");
}
STORAGE.put(key, stack);
}
@SuppressWarnings("unused")
public static void insertEnter(Object key) {
System.out.println("insert ->: " + key);
CapturedStack stack = STORAGE.get(key);
Deque<InsertMatch> currentStacks = CURRENT_STACKS.get();
if (stack != null) {
currentStacks.add(new InsertMatch(stack, ourJavaLangAccess.getStackTraceDepth(new Throwable())));
System.out.println("insert ->: stack saved (" + currentStacks.size() + ")");
}
else {
currentStacks.add(InsertMatch.EMPTY);
System.out.println("insert ->: no stack found (" + currentStacks.size() + ")");
}
}
@SuppressWarnings("unused")
public static void insertExit(Object key) {
System.out.println("insert <-: " + key);
Deque<InsertMatch> currentStacks = CURRENT_STACKS.get();
if (currentStacks.isEmpty()) {
System.out.println("insert error, empty stack: " + key);
return;
}
currentStacks.removeLast();
System.out.println("insert <-: stack removed (" + currentStacks.size() + ")");
}
// TODO: may save space using a separate CapturedStack w/o replace stack when can
private static class CapturedStack {
private final Throwable myException;
private final InsertMatch myInsertMatch;
public CapturedStack(Throwable exception) {
this(exception, InsertMatch.EMPTY);
}
public CapturedStack(Throwable exception, InsertMatch insertMatch) {
myException = exception;
myInsertMatch = insertMatch;
}
List<StackTraceElement> getStackTrace() {
StackTraceElement[] stackTrace = myException.getStackTrace();
if (myInsertMatch == null || myInsertMatch == InsertMatch.EMPTY) {
return Arrays.asList(stackTrace).subList(1, stackTrace.length);
}
else {
List<StackTraceElement> insertStack = myInsertMatch.myStack.getStackTrace();
int insertPos = stackTrace.length - myInsertMatch.myDepth + 2;
ArrayList<StackTraceElement> res = new ArrayList<>(insertPos + insertStack.size() + 1);
res.addAll(Arrays.asList(stackTrace).subList(1, insertPos));
res.add(null);
res.addAll(insertStack);
return res;
}
}
}
private static class InsertMatch {
private final CapturedStack myStack;
private final int myDepth;
static final InsertMatch EMPTY = new InsertMatch(null, 0);
private InsertMatch(CapturedStack stack, int depth) {
myStack = stack;
myDepth = depth;
}
}
// to be run from the debugger
@SuppressWarnings("unused")
public static Object[][] getRelatedStack(Object key) {
Exception exception = STORAGE.get(key);
StackTraceElement[] stackTrace = exception.getStackTrace();
Object[][] res = new Object[stackTrace.length][];
for (int i = 0; i < stackTrace.length; i++) {
StackTraceElement elem = stackTrace[i];
res[i] = new Object[]{elem.getClassName(), elem.getFileName(), elem.getMethodName(), String.valueOf(elem.getLineNumber())};
CapturedStack stack = STORAGE.get(key);
if (stack == null) {
return null;
}
List<StackTraceElement> stackTrace = stack.getStackTrace();
Object[][] res = new Object[stackTrace.size()][];
for (int i = 0; i < stackTrace.size(); i++) {
StackTraceElement elem = stackTrace.get(i);
if (elem == null) {
res[i] = null;
}
else {
res[i] = new Object[]{elem.getClassName(), elem.getFileName(), elem.getMethodName(), String.valueOf(elem.getLineNumber())};
}
}
return res;
}
@@ -23,9 +23,11 @@ import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.ProtectionDomain;
import java.util.*;
import java.util.function.Supplier;
import java.util.jar.JarFile;
/**
@@ -33,12 +35,20 @@ import java.util.jar.JarFile;
*/
public class CaptureAgent {
private static Instrumentation ourInstrumentation;
private static volatile Map<String, List<CapturePoint>> myCapturePoints = new HashMap<>();
static {
CapturePoint invokeLater = new CapturePoint("javax/swing/SwingUtilities", "invokeLater", 0);
myCapturePoints.put(invokeLater.myClassName, Collections.singletonList(invokeLater));
}
private static volatile Map<String, List<InsertPoint>> myInsertPoints = new HashMap<>();
static {
InsertPoint invokeLater = new InsertPoint("java/awt/event/InvocationEvent", "dispatch", "runnable", "Ljava/lang/Runnable;");
myInsertPoints.put(invokeLater.myClassName, Collections.singletonList(invokeLater));
}
public static void premain(String args, Instrumentation instrumentation) throws IOException {
ourInstrumentation = instrumentation;
instrumentation.appendToBootstrapClassLoaderSearch(createTempJar("debugger-agent-storage.jar"));
@@ -47,6 +57,10 @@ public class CaptureAgent {
System.out.println("Capture agent: ready");
}
private static <T> List<T> getNotNull(List<T> list) {
return list != null ? list : Collections.emptyList();
}
private static class CaptureTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader,
@@ -54,14 +68,36 @@ public class CaptureAgent {
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
List<CapturePoint> capturePoints = myCapturePoints.get(className);
if (!capturePoints.isEmpty()) {
List<CapturePoint> capturePoints = getNotNull(myCapturePoints.get(className));
List<InsertPoint> insertPoints = getNotNull(myInsertPoints.get(className));
if (!capturePoints.isEmpty() || !insertPoints.isEmpty()) {
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
for (CapturePoint point : capturePoints) {
reader.accept(new CaptureInstrumentor(Opcodes.ASM6, writer, point), 0);
try {
reader.accept(new CaptureInstrumentor(Opcodes.ASM6, writer, point), 0);
} catch (Exception e) {
e.printStackTrace();
}
}
return writer.toByteArray();
for (InsertPoint point : insertPoints) {
try {
reader.accept(new InsertInstrumentor(Opcodes.ASM6, writer, point), 0);
} catch (Exception e) {
e.printStackTrace();
}
}
byte[] bytes = writer.toByteArray();
try {
Path path = new File("instrumented_" + className.replaceAll("/", "_") + ".class").toPath();
Files.write(path, bytes);
}
catch (IOException e) {
e.printStackTrace();
}
return bytes;
}
return null;
}
@@ -79,18 +115,13 @@ public class CaptureAgent {
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if (capturePoint.myMethodName.equals(name)) {
System.out.println("Capture agent: instrumented " + capturePoint.myClassName + "." + name);
System.out.println("Capture agent: instrumented capture point at " + capturePoint.myClassName + "." + name);
return new MethodVisitor(api, mv) {
@Override
public void visitCode() {
visitFieldInsn(Opcodes.GETSTATIC, CaptureStorage.class.getName().replaceAll("\\.", "/"), "STORAGE", "Ljava/util/Map;");
visitVarInsn(Opcodes.ALOAD, capturePoint.myParamSlotId);
visitTypeInsn(Opcodes.NEW, "java/lang/Exception");
visitInsn(Opcodes.DUP);
visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Exception", "<init>", "()V", false);
visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Map", "put",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", true);
visitInsn(Opcodes.POP);
visitMethodInsn(Opcodes.INVOKESTATIC, CaptureStorage.class.getName().replaceAll("\\.", "/"), "capture",
"(Ljava/lang/Object;)V", false);
super.visitCode();
}
};
@@ -99,15 +130,104 @@ public class CaptureAgent {
}
}
private static class InsertInstrumentor extends ClassVisitor {
private InsertPoint myInsertPoint;
Supplier<MethodVisitor> myVisitMethod = null;
String myDesc;
public InsertInstrumentor(int api, ClassVisitor cv, InsertPoint insertPoint) {
super(api, cv);
this.myInsertPoint = insertPoint;
}
private static String getNewName(String name) {
return name + "$$$capture";
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (myInsertPoint.myMethodName.equals(name)) {
MethodVisitor mv = super.visitMethod(access, getNewName(name), desc, signature, exceptions);
myDesc = desc;
myVisitMethod = () -> super.visitMethod(access, name, desc, signature, exceptions);
System.out.println("Capture agent: instrumented insert point at " + myInsertPoint.myClassName + "." + name);
return mv;
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
@Override
public void visitEnd() {
if (myVisitMethod != null) {
MethodVisitor mv = myVisitMethod.get();
Label start = new Label();
mv.visitLabel(start);
insertEnter(mv);
mv.visitVarInsn(Opcodes.ALOAD, 0);
// TODO: mv.loadArgs();
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, myInsertPoint.myClassName, getNewName(myInsertPoint.myMethodName), myDesc, false);
Label end = new Label();
mv.visitLabel(end);
// regular exit
insertExit(mv);
mv.visitInsn(Opcodes.RETURN);
Label catchLabel = new Label();
mv.visitLabel(catchLabel);
mv.visitTryCatchBlock(start, end, catchLabel, null);
// exception exit
insertExit(mv);
mv.visitInsn(Opcodes.ATHROW);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
}
private void insertEnter(MethodVisitor mv) {
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, myInsertPoint.myClassName, myInsertPoint.myField, myInsertPoint.myFieldDesc);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, CaptureStorage.class.getName().replaceAll("\\.", "/"), "insertEnter",
"(Ljava/lang/Object;)V", false);
}
private void insertExit(MethodVisitor mv) {
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, myInsertPoint.myClassName, myInsertPoint.myField, myInsertPoint.myFieldDesc);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, CaptureStorage.class.getName().replaceAll("\\.", "/"), "insertExit",
"(Ljava/lang/Object;)V", false);
}
}
static class CapturePoint {
final String myClassName;
final String myMethodName;
final int myParamSlotId;
public CapturePoint(String myClassName, String myMethodName, int myParamSlotId) {
this.myClassName = myClassName;
this.myMethodName = myMethodName;
this.myParamSlotId = myParamSlotId;
public CapturePoint(String className, String methodName, int paramSlotId) {
this.myClassName = className;
this.myMethodName = methodName;
this.myParamSlotId = paramSlotId;
}
}
static class InsertPoint {
final String myClassName;
final String myMethodName;
final String myField;
final String myFieldDesc;
public InsertPoint(String className, String methodName, String field, String fieldDesc) {
this.myClassName = className;
this.myMethodName = methodName;
this.myField = field;
myFieldDesc = fieldDesc;
}
}
@@ -303,10 +303,15 @@ public class StackCapturingLineBreakpoint extends WildcardMethodBreakpoint {
List<Value> values = ((ArrayReference)resArray).getValues();
List<StackFrameItem> res = new ArrayList<>(values.size());
for (Value value : values) {
List<Value> values1 = ((ArrayReference)value).getValues();
res.add(new ProcessStackFrameItem(getStringRefValue((StringReference)values1.get(0)),
getStringRefValue((StringReference)values1.get(2)),
Integer.parseInt(((StringReference)values1.get(3)).value())));
if (value == null) {
res.add(null);
}
else {
List<Value> values1 = ((ArrayReference)value).getValues();
res.add(new ProcessStackFrameItem(getStringRefValue((StringReference)values1.get(0)),
getStringRefValue((StringReference)values1.get(2)),
Integer.parseInt(((StringReference)values1.get(3)).value())));
}
}
return res;
}
@@ -346,6 +351,11 @@ public class StackCapturingLineBreakpoint extends WildcardMethodBreakpoint {
public String method() {
return myMethod;
}
@Override
public String toString() {
return myClass + "." + myMethod + ":" + myLine;
}
}
@Nullable