[debugger] fallback to the direct evaluation if helper fails and report helper stack in the error message

GitOrigin-RevId: e8162cd8f310cf870b456abe2a76da043fe3b726
This commit is contained in:
Egor Ushakov
2024-11-11 20:12:57 +00:00
committed by intellij-monorepo-bot
parent 9477778580
commit a8bcc8b7db
4 changed files with 195 additions and 154 deletions
@@ -5,7 +5,6 @@ import com.intellij.Patches;
import com.intellij.debugger.*;
import com.intellij.debugger.actions.DebuggerAction;
import com.intellij.debugger.engine.evaluation.*;
import com.intellij.debugger.engine.evaluation.expression.BoxingEvaluator;
import com.intellij.debugger.engine.evaluation.expression.RetryEvaluationException;
import com.intellij.debugger.engine.events.DebuggerCommandImpl;
import com.intellij.debugger.engine.events.DebuggerContextCommandImpl;
@@ -63,11 +62,13 @@ import com.intellij.psi.CommonClassNames;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.rt.debugger.MethodInvoker;
import com.intellij.ui.awt.AnchoredPoint;
import com.intellij.ui.classFilter.ClassFilter;
import com.intellij.ui.classFilter.DebuggerClassFilterProvider;
import com.intellij.util.*;
import com.intellij.util.Alarm;
import com.intellij.util.EventDispatcher;
import com.intellij.util.ObjectUtils;
import com.intellij.util.SingleEdtTaskScheduler;
import com.intellij.util.concurrency.EdtScheduler;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.containers.ContainerUtil;
@@ -107,6 +108,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import static com.intellij.debugger.engine.MethodInvokeUtilsKt.tryInvokeWithHelper;
public abstract class DebugProcessImpl extends UserDataHolderBase implements DebugProcess {
private static final Logger LOG = Logger.getInstance(DebugProcessImpl.class);
@@ -1483,85 +1486,26 @@ public abstract class DebugProcessImpl extends UserDataHolderBase implements Deb
@NotNull List<? extends Value> args,
final int invocationOptions,
boolean internalEvaluate) throws EvaluateException {
if (!internalEvaluate && shouldInvokeWithHelper(method, invocationOptions)) {
return invokeWithHelper(method.declaringType(), objRef, method, args, (EvaluationContextImpl)evaluationContext);
InvocationResult result =
tryInvokeWithHelper(method.declaringType(), objRef, method, args, (EvaluationContextImpl)evaluationContext, invocationOptions, internalEvaluate);
if (result.isSuccess()) {
return result.getValue();
}
else {
return new InvokeCommand<>(method, args, (EvaluationContextImpl)evaluationContext) {
@Override
protected Value invokeMethod(ThreadReference thread, int invokePolicy, Method method, List<? extends Value> args)
throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException {
if (LOG.isDebugEnabled()) {
LOG.debug("Invoking " + objRef.type().name() + "." + method.name());
}
//noinspection SSBasedInspection
return objRef.invokeMethod(thread, method, args, invokePolicy | invocationOptions);
return new InvokeCommand<>(method, args, (EvaluationContextImpl)evaluationContext) {
@Override
protected Value invokeMethod(ThreadReference thread, int invokePolicy, Method method, List<? extends Value> args)
throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException {
if (LOG.isDebugEnabled()) {
LOG.debug("Invoking " + objRef.type().name() + "." + method.name());
}
}.start(internalEvaluate);
}
}
private static boolean shouldInvokeWithHelper(@NotNull Method method, int invocationOptions) {
return Registry.is("debugger.evaluate.method.helper") &&
!BitUtil.isSet(invocationOptions, ObjectReference.INVOKE_NONVIRTUAL) && // TODO: support
!DebuggerUtils.isPrimitiveType(method.returnTypeName()) &&
(!DebuggerUtilsEx.isVoid(method) || method.isConstructor()) &&
!"clone".equals(method.name());
}
private static @Nullable Value invokeWithHelper(@NotNull ReferenceType type,
@Nullable ObjectReference objRef,
@NotNull Method method,
@NotNull List<? extends Value> originalArgs,
@NotNull EvaluationContextImpl evaluationContext) throws EvaluateException {
DebugProcessImpl debugProcess = evaluationContext.getDebugProcess();
ArrayList<Value> invokerArgs = new ArrayList<>();
ReferenceType lookupClass =
debugProcess.findClass(evaluationContext, "java.lang.invoke.MethodHandles$Lookup", evaluationContext.getClassLoader());
ObjectReference implLookup = (ObjectReference)lookupClass.getValue(DebuggerUtils.findField(lookupClass, "IMPL_LOOKUP"));
invokerArgs.add(implLookup); // lookup
invokerArgs.add(type.classObject()); // class
invokerArgs.add(objRef); // object
invokerArgs.add(DebuggerUtilsEx.mirrorOfString(method.name() + ";" + method.signature(),
evaluationContext)); // method name and descriptor
invokerArgs.add(method.declaringType().classLoader()); // method's declaring type class loader to be able to resolve parameter types
// argument values
List<Value> args = new ArrayList<>(originalArgs);
if (method.isVarArgs()) {
// If vararg is Object... and an array of Objects is passed, we need to unwrap it or we'll not be able to distinguish what was passed later
List<String> argumentTypeNames = method.argumentTypeNames();
int lastIndex = argumentTypeNames.size() - 1;
if (args.size() == lastIndex + 1 && args.get(lastIndex) instanceof ArrayReference arrayRef &&
argumentTypeNames.get(lastIndex).startsWith(CommonClassNames.JAVA_LANG_OBJECT)) {
args.remove(lastIndex);
args.addAll(arrayRef.getValues());
//noinspection SSBasedInspection
return objRef.invokeMethod(thread, method, args, invokePolicy | invocationOptions);
}
}
List<Value> boxedArgs = new ArrayList<>(args.size());
for (Value arg : args) {
boxedArgs.add((Value)BoxingEvaluator.box(arg, evaluationContext));
}
if (boxedArgs.size() < 10) {
invokerArgs.addAll(boxedArgs); // args
return DebuggerUtilsImpl.invokeHelperMethod(evaluationContext, MethodInvoker.class, "invoke" + boxedArgs.size(), invokerArgs, false);
}
else {
ArrayType objectArrayClass = (ArrayType)debugProcess.findClass(
evaluationContext,
CommonClassNames.JAVA_LANG_OBJECT + "[]",
evaluationContext.getClassLoader());
invokerArgs.add(DebuggerUtilsEx.mirrorOfArray(objectArrayClass, boxedArgs, evaluationContext)); // args
return DebuggerUtilsImpl.invokeHelperMethod(evaluationContext, MethodInvoker.class, "invoke", invokerArgs, false);
}
}.start(internalEvaluate);
}
private static ThreadReferenceProxy getEvaluationThread(final EvaluationContext evaluationContext) throws EvaluateException {
@NotNull
static ThreadReferenceProxy getEvaluationThread(final EvaluationContext evaluationContext) throws EvaluateException {
ThreadReferenceProxy fromStackFrame =
ObjectUtils.doIfNotNull(evaluationContext.getFrameProxy(), stackFrameProxy -> stackFrameProxy.threadProxy());
SuspendContextImpl suspendContext = (SuspendContextImpl)evaluationContext.getSuspendContext();
@@ -1597,53 +1541,53 @@ public abstract class DebugProcessImpl extends UserDataHolderBase implements Deb
@NotNull List<? extends Value> args,
int extraInvocationOptions,
boolean internalEvaluate) throws EvaluateException {
if (!internalEvaluate && shouldInvokeWithHelper(method, extraInvocationOptions)) {
return invokeWithHelper(classType, null, method, args, (EvaluationContextImpl)evaluationContext);
InvocationResult result =
tryInvokeWithHelper(classType, null, method, args, (EvaluationContextImpl)evaluationContext, extraInvocationOptions, internalEvaluate);
if (result.isSuccess()) {
return result.getValue();
}
else {
return new InvokeCommand<>(method, args, (EvaluationContextImpl)evaluationContext) {
@Override
protected Value invokeMethod(ThreadReference thread, int invokePolicy, Method method, List<? extends Value> args)
throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException {
if (LOG.isDebugEnabled()) {
LOG.debug("Invoking " + classType.name() + "." + method.name());
}
//noinspection SSBasedInspection
return classType.invokeMethod(thread, method, args, invokePolicy | extraInvocationOptions);
return new InvokeCommand<>(method, args, (EvaluationContextImpl)evaluationContext) {
@Override
protected Value invokeMethod(ThreadReference thread, int invokePolicy, Method method, List<? extends Value> args)
throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException {
if (LOG.isDebugEnabled()) {
LOG.debug("Invoking " + classType.name() + "." + method.name());
}
}.start(internalEvaluate);
}
//noinspection SSBasedInspection
return classType.invokeMethod(thread, method, args, invokePolicy | extraInvocationOptions);
}
}.start(internalEvaluate);
}
public Value invokeMethod(EvaluationContext evaluationContext,
InterfaceType interfaceType,
Method method,
List<? extends Value> args) throws EvaluateException {
if (shouldInvokeWithHelper(method, 0)) {
return invokeWithHelper(interfaceType, null, method, args, (EvaluationContextImpl)evaluationContext);
InvocationResult result =
tryInvokeWithHelper(interfaceType, null, method, args, (EvaluationContextImpl)evaluationContext, 0, false);
if (result.isSuccess()) {
return result.getValue();
}
else {
return new InvokeCommand<>(method, args, (EvaluationContextImpl)evaluationContext) {
@Override
protected Value invokeMethod(ThreadReference thread, int invokePolicy, Method method, List<? extends Value> args)
throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException {
if (LOG.isDebugEnabled()) {
LOG.debug("Invoking " + interfaceType.name() + "." + method.name());
}
try {
//noinspection SSBasedInspection
return interfaceType.invokeMethod(thread, method, args, invokePolicy);
}
catch (LinkageError e) {
throw new IllegalStateException("Interface method invocation is not supported in JVM " +
SystemInfo.JAVA_VERSION +
". Use JVM 1.8.0_45 or higher to run " +
ApplicationNamesInfo.getInstance().getFullProductName());
}
return new InvokeCommand<>(method, args, (EvaluationContextImpl)evaluationContext) {
@Override
protected Value invokeMethod(ThreadReference thread, int invokePolicy, Method method, List<? extends Value> args)
throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException {
if (LOG.isDebugEnabled()) {
LOG.debug("Invoking " + interfaceType.name() + "." + method.name());
}
}.start(false);
}
try {
//noinspection SSBasedInspection
return interfaceType.invokeMethod(thread, method, args, invokePolicy);
}
catch (LinkageError e) {
throw new IllegalStateException("Interface method invocation is not supported in JVM " +
SystemInfo.JAVA_VERSION +
". Use JVM 1.8.0_45 or higher to run " +
ApplicationNamesInfo.getInstance().getFullProductName());
}
}
}.start(false);
}
@@ -1673,23 +1617,23 @@ public abstract class DebugProcessImpl extends UserDataHolderBase implements Deb
@NotNull List<? extends Value> args,
final int invocationOptions,
boolean internalEvaluate) throws EvaluateException {
if (!internalEvaluate && shouldInvokeWithHelper(method, invocationOptions)) {
return (ObjectReference)invokeWithHelper(classType, null, method, args, (EvaluationContextImpl)evaluationContext);
InvocationResult result =
tryInvokeWithHelper(classType, null, method, args, (EvaluationContextImpl)evaluationContext, invocationOptions, internalEvaluate);
if (result.isSuccess()) {
return (ObjectReference)result.getValue();
}
else {
InvokeCommand<ObjectReference> invokeCommand = new InvokeCommand<>(method, args, (EvaluationContextImpl)evaluationContext) {
@Override
protected ObjectReference invokeMethod(ThreadReference thread, int invokePolicy, Method method, List<? extends Value> args)
throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException {
if (LOG.isDebugEnabled()) {
LOG.debug("New instance " + classType.name() + "." + method.name());
}
//noinspection SSBasedInspection
return classType.newInstance(thread, method, args, invokePolicy | invocationOptions);
InvokeCommand<ObjectReference> invokeCommand = new InvokeCommand<>(method, args, (EvaluationContextImpl)evaluationContext) {
@Override
protected ObjectReference invokeMethod(ThreadReference thread, int invokePolicy, Method method, List<? extends Value> args)
throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException {
if (LOG.isDebugEnabled()) {
LOG.debug("New instance " + classType.name() + "." + method.name());
}
};
return invokeCommand.start(internalEvaluate);
}
//noinspection SSBasedInspection
return classType.newInstance(thread, method, args, invokePolicy | invocationOptions);
}
};
return invokeCommand.start(internalEvaluate);
}
public void clearCashes(@NotNull SuspendContextImpl context) {
@@ -0,0 +1,118 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.debugger.engine
import com.intellij.debugger.engine.DebuggerUtils.isPrimitiveType
import com.intellij.debugger.engine.MethodInvokeUtils.getHelperExceptionStackTrace
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.engine.evaluation.expression.BoxingEvaluator
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.impl.DebuggerUtilsEx.isVoid
import com.intellij.debugger.impl.DebuggerUtilsImpl
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.CommonClassNames
import com.intellij.rt.debugger.MethodInvoker
import com.intellij.util.BitUtil.isSet
import com.sun.jdi.*
import com.sun.jdi.ObjectReference.INVOKE_NONVIRTUAL
object MethodInvokeUtils {
fun getHelperExceptionStackTrace(evaluationContext: EvaluationContextImpl, e: Exception): String? {
e as? EvaluateException ?: return null
val exceptionFromTargetVM = e.exceptionFromTargetVM ?: return null
var exceptionStack = DebuggerUtilsImpl.getExceptionText(evaluationContext, exceptionFromTargetVM)
if (!exceptionStack.isNullOrEmpty()) {
// drop user frames
val currentStackDepth = DebugProcessImpl.getEvaluationThread(evaluationContext).frameCount()
val lines = StringUtil.splitByLines(exceptionStack) // exclude empty lines
if (lines.size > currentStackDepth) {
return lines.asSequence().take(lines.size - currentStackDepth).joinToString(separator = "\n")
}
else {
logger<MethodInvokeUtils>().error("Invalid helper stack (expected currentStackDepth = ${currentStackDepth}) : ${exceptionStack}")
return exceptionStack
}
}
return null
}
}
@Throws(EvaluateException::class)
internal fun tryInvokeWithHelper(
type: ReferenceType,
objRef: ObjectReference?,
method: Method,
originalArgs: List<Value?>,
evaluationContext: EvaluationContextImpl,
invocationOptions: Int,
internalEvaluate: Boolean,
): InvocationResult {
if (internalEvaluate ||
!Registry.`is`("debugger.evaluate.method.helper") ||
isSet(invocationOptions, INVOKE_NONVIRTUAL) || //TODO: support
isPrimitiveType(method.returnTypeName()) ||
(isVoid(method) && !method.isConstructor) ||
"clone" == method.name()) {
return InvocationResult(false, null)
}
val debugProcess = evaluationContext.debugProcess
val invokerArgs = mutableListOf<Value?>()
val lookupClass =
debugProcess.findClass(evaluationContext, "java.lang.invoke.MethodHandles\$Lookup", evaluationContext.getClassLoader())
val implLookup = lookupClass.getValue(DebuggerUtils.findField(lookupClass, "IMPL_LOOKUP")) as ObjectReference
invokerArgs.add(implLookup) // lookup
invokerArgs.add(type.classObject()) // class
invokerArgs.add(objRef) // object
invokerArgs.add(DebuggerUtilsEx.mirrorOfString(method.name() + ";" + method.signature(), evaluationContext)) // method name and descriptor
invokerArgs.add(method.declaringType().classLoader()) // method's declaring type class loader to be able to resolve parameter types
// argument values
val args = originalArgs.toMutableList()
if (method.isVarArgs) {
// If vararg is Object... and an array of Objects is passed, we need to unwrap it or we'll not be able to distinguish what was passed later
val argumentTypeNames = method.argumentTypeNames()
(args.lastOrNull() as? ArrayReference)?.let {
if (args.size == argumentTypeNames.size && argumentTypeNames.last().startsWith(CommonClassNames.JAVA_LANG_OBJECT)) {
args.removeLast()
args.addAll(it.values)
}
}
}
val boxedArgs = args.map { BoxingEvaluator.box(it, evaluationContext) as Value? }
var helperMethodName = "invoke"
if (boxedArgs.size > 10) {
val objectArrayClass = debugProcess.findClass(
evaluationContext,
CommonClassNames.JAVA_LANG_OBJECT + "[]",
evaluationContext.getClassLoader()) as ArrayType
invokerArgs.add(DebuggerUtilsEx.mirrorOfArray(objectArrayClass, boxedArgs, evaluationContext)) // args as array
}
else {
helperMethodName = "invoke${boxedArgs.size}"
invokerArgs.addAll(boxedArgs) // add all args directly to the helper args
}
try {
return InvocationResult(true, DebuggerUtilsImpl.invokeHelperMethod(evaluationContext, MethodInvoker::class.java, helperMethodName, invokerArgs, false))
}
catch (e: Exception) {
val helperExceptionStackTrace = getHelperExceptionStackTrace(evaluationContext, e)
if (helperExceptionStackTrace?.contains(method.name() + "(") == true) {
throw e
}
DebuggerUtilsImpl.logError("Exception from helper: ${e.message}", e,
*listOfNotNull(helperExceptionStackTrace).toTypedArray()) // log helper exception if available
return InvocationResult(false, null)
}
}
internal data class InvocationResult(@get:JvmName("isSuccess") val success: Boolean, val value: Value?)
@@ -10,8 +10,4 @@ public final class ExceptionDebugHelper {
t.printStackTrace(new PrintWriter(writer));
return writer.getBuffer().toString();
}
public static int getCurrentThreadStackDepth() {
return Thread.currentThread().getStackTrace().length;
}
}
@@ -4,15 +4,14 @@ package org.jetbrains.kotlin.idea.debugger.coroutine
import com.intellij.debugger.actions.AsyncStacksToggleAction
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.MethodInvokeUtils
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.SuspendManagerUtil
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.impl.DebuggerUtilsEx
import com.intellij.debugger.impl.DebuggerUtilsImpl
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.util.registry.Registry
import com.intellij.rt.debugger.ExceptionDebugHelper
import com.intellij.rt.debugger.coroutines.CoroutinesDebugHelper
import com.intellij.xdebugger.frame.XStackFrame
import com.intellij.xdebugger.impl.XDebugSessionImpl
@@ -196,25 +195,9 @@ class CoroutineStackFrameInterceptor : StackFrameInterceptor {
try {
return DebuggerUtilsImpl.invokeHelperMethod(context.evaluationContext, helperClass, methodName, args)
} catch (e: Exception) {
if (e is EvaluateException && e.exceptionFromTargetVM != null) {
var exceptionStack = DebuggerUtilsImpl.getExceptionText(context.evaluationContext, e.exceptionFromTargetVM!!)
if (exceptionStack != null) {
// drop user frames
val currentStackDepth = (DebuggerUtilsImpl.invokeHelperMethod(
context.evaluationContext,
ExceptionDebugHelper::class.java,
"getCurrentThreadStackDepth",
emptyList()
) as IntegerValue).value()
val lines = exceptionStack.lines()
if (lines.size > currentStackDepth) {
exceptionStack = lines.subList(0, lines.size - currentStackDepth + 1).joinToString(separator = "\n")
}
DebuggerUtilsImpl.logError(e.message, e, exceptionStack)
return null
}
}
DebuggerUtilsImpl.logError(e) // for now log everything
val helperExceptionStackTrace = MethodInvokeUtils.getHelperExceptionStackTrace(context.evaluationContext, e)
DebuggerUtilsImpl.logError("Exception from helper: ${e.message}", e,
*listOfNotNull(helperExceptionStackTrace).toTypedArray()) // log helper exception if available
}
return null
}