debugger agent prototype, for now works only in debug mode

This commit is contained in:
Egor.Ushakov
2017-09-22 13:56:01 +03:00
parent e511e792be
commit ed898a983e
11 changed files with 351 additions and 15 deletions
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
@@ -0,0 +1,47 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.rt.debugger.agent;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author egor
*/
public class CaptureStorage {
private static final int MAX_STORED_STACKS = 1000;
public static final Map<Object, Exception> STORAGE = Collections.synchronizedMap(new LinkedHashMap<Object, Exception>() {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_STORED_STACKS;
}
});
// 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())};
}
return res;
}
}
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Premain-Class: com.intellij.rt.debugger.agent.CaptureAgent
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="ASM" level="project" />
<orderEntry type="module" module-name="debugger-agent-storage" />
</component>
</module>
@@ -0,0 +1,161 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.rt.debugger.agent;
import org.jetbrains.org.objectweb.asm.*;
import java.io.File;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.security.ProtectionDomain;
import java.util.*;
import java.util.jar.JarFile;
/**
* @author egor
*/
public class CaptureAgent {
private static Instrumentation OurInstrumentation;
private static volatile List<CapturePoint> myCapturePoints = Arrays.asList(new CapturePoint
("javax/swing/SwingUtilities", "invokeLater", 0)
// ,
// new CapturePoint("Test", "foo")
);
public static void premain(String args, Instrumentation instrumentation) throws IOException {
OurInstrumentation = instrumentation;
instrumentation.appendToBootstrapClassLoaderSearch(createTempJar("debugger-agent-storage.jar"));
instrumentation.appendToSystemClassLoaderSearch(createTempJar("asm-all.jar"));
instrumentation.addTransformer(new CaptureTransformer());
System.out.println("Capture agent: ready");
}
private static class CaptureTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
// TODO: speedup
for (CapturePoint capturePoint : myCapturePoints) {
if (capturePoint.myClassName.equals(className)) {
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
CaptureInstrumentor visitor = new CaptureInstrumentor(Opcodes.ASM6, writer, capturePoint);
reader.accept(visitor, 0);
return writer.toByteArray();
}
}
return null;
}
}
private static class CaptureInstrumentor extends ClassVisitor {
private CapturePoint capturePoint;
public CaptureInstrumentor(int api, ClassVisitor cv, CapturePoint capturePoint) {
super(api, cv);
this.capturePoint = capturePoint;
}
@Override
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);
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);
super.visitCode();
}
};
}
return mv;
}
}
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;
}
}
// TODO: these files are not deleted even if deleteOnExit or anything else, we need to separate jars
private static JarFile createTempJar(String name) throws IOException {
File tempJar = File.createTempFile("Capture", ".jar");
Files.copy(CaptureAgent.class.getClassLoader().getResourceAsStream(name), tempJar.toPath(),
StandardCopyOption.REPLACE_EXISTING);
JarFile res = new JarFile(tempJar);
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
try {
res.close();
} catch (IOException e) {
e.printStackTrace();
}
tempJar.delete();
}
});
return res;
}
// to be run from the debugger
@SuppressWarnings("unused")
public static void setCapturePoints(Object[][] capturePoints) throws UnmodifiableClassException {
Set<String> classNames = new HashSet<>();
for (CapturePoint point : myCapturePoints) {
classNames.add(point.myClassName);
}
List<CapturePoint> points = new ArrayList<>(capturePoints.length);
for (Object[] capturePoint : capturePoints) {
String className = (String)capturePoint[0];
classNames.add(className);
points.add(new CapturePoint(className, (String)capturePoint[1], (int)capturePoint[2]));
}
myCapturePoints = points;
List<Class> classes = new ArrayList<>(capturePoints.length);
for (String name : classNames) {
try {
classes.add(Class.forName(name));
}
catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
OurInstrumentation.retransformClasses(classes.toArray(new Class[0]));
}
}
+1
View File
@@ -22,6 +22,7 @@
<orderEntry type="module" module-name="platform-impl" />
<orderEntry type="module" module-name="util" />
<orderEntry type="module" module-name="jps-builders" />
<orderEntry type="module" module-name="debugger-agent-storage" />
</component>
<component name="copyright">
<Base>
@@ -208,7 +208,7 @@ public class JavaExecutionStack extends XExecutionStack {
// replace the rest with the related stack (if available)
if (Registry.is("debugger.capture.points") && frame instanceof JavaStackFrame) {
List<StackFrameItem> relatedStack = StackCapturingLineBreakpoint.getRelatedStack(frameProxy, suspendContext);
List<StackFrameItem> relatedStack = StackCapturingLineBreakpoint.getRelatedStack(frameProxy, suspendContext, true);
if (!ContainerUtil.isEmpty(relatedStack)) {
int i = 0;
boolean separator = true;
@@ -49,11 +49,15 @@ import com.intellij.openapi.projectRoots.ex.JavaSdkUtil;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiClass;
import com.intellij.rt.debugger.agent.CaptureStorage;
import com.intellij.util.EventDispatcher;
import com.intellij.util.Function;
import com.intellij.util.PathUtil;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import org.jdom.Element;
@@ -466,6 +470,16 @@ public class DebuggerManagerImpl extends DebuggerManagerEx implements Persistent
ApplicationManager.getApplication().runReadAction(() -> {
JavaSdkUtil.addRtJar(parameters.getClassPath());
if (Registry.is("debugger.capture.points.agent")) {
String path = PathUtil.getJarPathForClass(CaptureStorage.class);
//TODO: for now works only in debug mode
String agent = "-javaagent:" +
FileUtil.toSystemDependentName(PathUtil.getParentPath(PathUtil.getParentPath(path))) +
"/artifacts/debugger_agent/debugger-agent.jar";
if (!parameters.getVMParametersList().hasParameter(agent)) {
parameters.getVMParametersList().add(agent);
}
}
final Sdk jdk = parameters.getJdk();
final boolean forceClassicVM = shouldForceClassicVM(jdk);
@@ -26,17 +26,20 @@ import com.intellij.debugger.settings.NodeRendererSettings;
import com.intellij.debugger.ui.breakpoints.StackCapturingLineBreakpoint;
import com.intellij.debugger.ui.tree.render.ClassRenderer;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.PsiClass;
import com.intellij.ui.ColoredTextContainer;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.util.PlatformIcons;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.EmptyIcon;
import com.intellij.util.ui.JBUI;
import com.intellij.xdebugger.XDebuggerUtil;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.frame.*;
import com.intellij.xdebugger.frame.presentation.XStringValuePresentation;
@@ -82,6 +85,11 @@ public class StackFrameItem {
return myLocation.declaringType().name();
}
@NotNull
public String method() {
return myLocation.method().name();
}
public int line() {
return DebuggerUtilsEx.getLineNumber(myLocation, false);
}
@@ -152,7 +160,7 @@ public class StackFrameItem {
StackFrameItem frameItem = new StackFrameItem(location, vars);
res.add(frameItem);
List<StackFrameItem> relatedStack = StackCapturingLineBreakpoint.getRelatedStack(frame, suspendContext);
List<StackFrameItem> relatedStack = StackCapturingLineBreakpoint.getRelatedStack(frame, suspendContext, false);
if (!ContainerUtil.isEmpty(relatedStack)) {
res.add(null); // separator
res.addAll(relatedStack);
@@ -239,14 +247,27 @@ public class StackFrameItem {
public CapturedStackFrame(DebugProcessImpl debugProcess, StackFrameItem item) {
DebuggerManagerThreadImpl.assertIsManagerThread();
mySourcePosition = DebuggerUtilsEx.toXSourcePosition(debugProcess.getPositionManager().getSourcePosition(item.myLocation));
myIsSynthetic = DebuggerUtils.isSynthetic(item.myLocation.method());
myIsInLibraryContent =
DebuggerUtilsEx.isInLibraryContent(mySourcePosition != null ? mySourcePosition.getFile() : null, debugProcess.getProject());
myPath = item.path();
myMethodName = item.myLocation.method().name();
myMethodName = item.method();
myLineNumber = item.line();
myVariables = item.myVariables;
Location location = item.myLocation;
if (location != null) {
mySourcePosition = DebuggerUtilsEx.toXSourcePosition(debugProcess.getPositionManager().getSourcePosition(location));
}
else {
mySourcePosition = ReadAction.compute(() -> {
PsiClass aClass = PositionManagerImpl.findClass(debugProcess.getProject(), item.path(), debugProcess.getSearchScope());
if (aClass != null) {
return XDebuggerUtil.getInstance().createPosition(aClass.getContainingFile().getVirtualFile(), myLineNumber - 1);
}
return null;
});
}
myIsSynthetic = location != null && DebuggerUtils.isSynthetic(location.method());
myIsInLibraryContent =
DebuggerUtilsEx.isInLibraryContent(mySourcePosition != null ? mySourcePosition.getFile() : null, debugProcess.getProject());
}
@Nullable
@@ -39,6 +39,7 @@ import com.intellij.openapi.util.ThrowableComputable;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.rt.debugger.agent.CaptureStorage;
import com.intellij.ui.SimpleColoredComponent;
import com.intellij.util.containers.ContainerUtil;
import com.sun.jdi.*;
@@ -48,10 +49,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.java.debugger.breakpoints.properties.JavaMethodBreakpointProperties;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
/**
@@ -245,10 +243,12 @@ public class StackCapturingLineBreakpoint extends WildcardMethodBreakpoint {
}
@Nullable
public static List<StackFrameItem> getRelatedStack(@NotNull StackFrameProxyImpl frame, @NotNull SuspendContextImpl suspendContext) {
public static List<StackFrameItem> getRelatedStack(@NotNull StackFrameProxyImpl frame,
@NotNull SuspendContextImpl suspendContext,
boolean checkInProcessData) {
DebugProcessImpl debugProcess = suspendContext.getDebugProcess();
Map<Object, List<StackFrameItem>> capturedStacks = debugProcess.getUserData(CAPTURED_STACKS);
if (ContainerUtil.isEmpty(capturedStacks)) {
if (ContainerUtil.isEmpty(capturedStacks) && !Registry.is("debugger.capture.points.agent")) {
return null;
}
List<StackCapturingLineBreakpoint> captureBreakpoints = debugProcess.getUserData(CAPTURE_BREAKPOINTS);
@@ -265,10 +265,18 @@ public class StackCapturingLineBreakpoint extends WildcardMethodBreakpoint {
if ((StringUtil.isEmpty(insertClassName) || StringUtil.equals(insertClassName, className)) &&
StringUtil.equals(b.myCapturePoint.myInsertMethodName, methodName)) {
try {
Value key = b.myInsertEvaluator.evaluate(new EvaluationContextImpl(suspendContext, frame));
EvaluationContextImpl evaluationContext = new EvaluationContextImpl(suspendContext, frame);
Value key = b.myInsertEvaluator.evaluate(evaluationContext);
List<StackFrameItem> items = null;
if (key instanceof ObjectReference) {
return capturedStacks.get(getKey((ObjectReference)key));
if (capturedStacks != null) {
items = capturedStacks.get(getKey((ObjectReference)key));
}
if (items == null && checkInProcessData) {
items = getProcessCapturedStack(key, evaluationContext);
}
}
return items;
}
catch (EvaluateException e) {
LOG.debug(e);
@@ -283,6 +291,63 @@ public class StackCapturingLineBreakpoint extends WildcardMethodBreakpoint {
return null;
}
private static List<StackFrameItem> getProcessCapturedStack(Value key, EvaluationContextImpl evaluationContext)
throws EvaluateException {
if (Registry.is("debugger.capture.points.agent")) {
DebugProcessImpl process = evaluationContext.getDebugProcess();
// TODO: cache class & method
ClassType captureClass = (ClassType)process.findClass(evaluationContext, CaptureStorage.class.getName(), null);
Method getRelatedStackMethod = captureClass.methodsByName("getRelatedStack").get(0);
Value resArray = process.invokeMethod(evaluationContext, captureClass, getRelatedStackMethod, Collections.singletonList(key), true);
if (resArray instanceof ArrayReference) {
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())));
}
return res;
}
}
return null;
}
private static String getStringRefValue(StringReference ref) {
return ref != null ? ref.value() : null;
}
private static class ProcessStackFrameItem extends StackFrameItem {
final String myClass;
final String myMethod;
final int myLine;
public ProcessStackFrameItem(String aClass, String method, int line) {
super(null, null);
myClass = aClass;
myMethod = method;
myLine = line;
}
@NotNull
@Override
public String path() {
return myClass;
}
@Override
public int line() {
return myLine;
}
@NotNull
@Override
public String method() {
return myMethod;
}
}
@Nullable
public static List<StackFrameItem> getRelatedStack(@Nullable ObjectReference key, @Nullable DebugProcessImpl process) {
if (process != null && key != null) {
@@ -297,6 +297,7 @@ debugger.emulate.method.breakpoints.description=Emulate method breakpoints with
debugger.intern.string.literals=false
debugger.intern.string.literals.description=Make string literal refer to the same instance of class String
debugger.capture.points=true
debugger.capture.points.agent=false
debugger.capture.points.annotations=false
debugger.resume.yourkit.threads=false
debugger.keep.step.requests=false