diff --git a/java/debugger/debugger-agent-storage/debugger-agent-storage.iml b/java/debugger/debugger-agent-storage/debugger-agent-storage.iml new file mode 100644 index 000000000000..c90834f2d607 --- /dev/null +++ b/java/debugger/debugger-agent-storage/debugger-agent-storage.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/java/debugger/debugger-agent-storage/src/com/intellij/rt/debugger/agent/CaptureStorage.java b/java/debugger/debugger-agent-storage/src/com/intellij/rt/debugger/agent/CaptureStorage.java new file mode 100644 index 000000000000..ade32caec0a6 --- /dev/null +++ b/java/debugger/debugger-agent-storage/src/com/intellij/rt/debugger/agent/CaptureStorage.java @@ -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 STORAGE = Collections.synchronizedMap(new LinkedHashMap() { + @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; + } +} diff --git a/java/debugger/debugger-agent/META-INF/MANIFEST.MF b/java/debugger/debugger-agent/META-INF/MANIFEST.MF new file mode 100644 index 000000000000..3c53b952c270 --- /dev/null +++ b/java/debugger/debugger-agent/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Premain-Class: com.intellij.rt.debugger.agent.CaptureAgent diff --git a/java/debugger/debugger-agent/debugger-agent.iml b/java/debugger/debugger-agent/debugger-agent.iml new file mode 100644 index 000000000000..8b0d822d1f09 --- /dev/null +++ b/java/debugger/debugger-agent/debugger-agent.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/debugger/debugger-agent/src/com/intellij/rt/debugger/agent/CaptureAgent.java b/java/debugger/debugger-agent/src/com/intellij/rt/debugger/agent/CaptureAgent.java new file mode 100644 index 000000000000..14267b3693b4 --- /dev/null +++ b/java/debugger/debugger-agent/src/com/intellij/rt/debugger/agent/CaptureAgent.java @@ -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 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", "", "()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 classNames = new HashSet<>(); + for (CapturePoint point : myCapturePoints) { + classNames.add(point.myClassName); + } + + List 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 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])); + } +} diff --git a/java/debugger/impl/debugger-impl.iml b/java/debugger/impl/debugger-impl.iml index a4a7834fe605..a11386710af3 100644 --- a/java/debugger/impl/debugger-impl.iml +++ b/java/debugger/impl/debugger-impl.iml @@ -22,6 +22,7 @@ + diff --git a/java/debugger/impl/src/com/intellij/debugger/engine/JavaExecutionStack.java b/java/debugger/impl/src/com/intellij/debugger/engine/JavaExecutionStack.java index 61f2027fa1ae..f7021dec8862 100644 --- a/java/debugger/impl/src/com/intellij/debugger/engine/JavaExecutionStack.java +++ b/java/debugger/impl/src/com/intellij/debugger/engine/JavaExecutionStack.java @@ -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 relatedStack = StackCapturingLineBreakpoint.getRelatedStack(frameProxy, suspendContext); + List relatedStack = StackCapturingLineBreakpoint.getRelatedStack(frameProxy, suspendContext, true); if (!ContainerUtil.isEmpty(relatedStack)) { int i = 0; boolean separator = true; diff --git a/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerManagerImpl.java b/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerManagerImpl.java index 488e5c4e3174..8192b6d6b17c 100644 --- a/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerManagerImpl.java +++ b/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerManagerImpl.java @@ -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); diff --git a/java/debugger/impl/src/com/intellij/debugger/memory/utils/StackFrameItem.java b/java/debugger/impl/src/com/intellij/debugger/memory/utils/StackFrameItem.java index 7ab32c520236..d7ed5baae392 100644 --- a/java/debugger/impl/src/com/intellij/debugger/memory/utils/StackFrameItem.java +++ b/java/debugger/impl/src/com/intellij/debugger/memory/utils/StackFrameItem.java @@ -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 relatedStack = StackCapturingLineBreakpoint.getRelatedStack(frame, suspendContext); + List 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 diff --git a/java/debugger/impl/src/com/intellij/debugger/ui/breakpoints/StackCapturingLineBreakpoint.java b/java/debugger/impl/src/com/intellij/debugger/ui/breakpoints/StackCapturingLineBreakpoint.java index c8be4a4369a4..64fc9b0e6ff7 100644 --- a/java/debugger/impl/src/com/intellij/debugger/ui/breakpoints/StackCapturingLineBreakpoint.java +++ b/java/debugger/impl/src/com/intellij/debugger/ui/breakpoints/StackCapturingLineBreakpoint.java @@ -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 getRelatedStack(@NotNull StackFrameProxyImpl frame, @NotNull SuspendContextImpl suspendContext) { + public static List getRelatedStack(@NotNull StackFrameProxyImpl frame, + @NotNull SuspendContextImpl suspendContext, + boolean checkInProcessData) { DebugProcessImpl debugProcess = suspendContext.getDebugProcess(); Map> capturedStacks = debugProcess.getUserData(CAPTURED_STACKS); - if (ContainerUtil.isEmpty(capturedStacks)) { + if (ContainerUtil.isEmpty(capturedStacks) && !Registry.is("debugger.capture.points.agent")) { return null; } List 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 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 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 values = ((ArrayReference)resArray).getValues(); + List res = new ArrayList<>(values.size()); + for (Value value : values) { + List 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 getRelatedStack(@Nullable ObjectReference key, @Nullable DebugProcessImpl process) { if (process != null && key != null) { diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 155f00bd77f3..808fb5431e17 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -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