From a8bcc8b7db151b1548135da5e4b34385b5bd0de0 Mon Sep 17 00:00:00 2001 From: Egor Ushakov Date: Mon, 11 Nov 2024 19:16:24 +0100 Subject: [PATCH] [debugger] fallback to the direct evaluation if helper fails and report helper stack in the error message GitOrigin-RevId: e8162cd8f310cf870b456abe2a76da043fe3b726 --- .../debugger/engine/DebugProcessImpl.java | 202 +++++++----------- .../debugger/engine/MethodInvokeUtils.kt | 118 ++++++++++ .../rt/debugger/ExceptionDebugHelper.java | 4 - .../CoroutineStackFrameInterceptor.kt | 25 +-- 4 files changed, 195 insertions(+), 154 deletions(-) create mode 100644 java/debugger/impl/src/com/intellij/debugger/engine/MethodInvokeUtils.kt diff --git a/java/debugger/impl/src/com/intellij/debugger/engine/DebugProcessImpl.java b/java/debugger/impl/src/com/intellij/debugger/engine/DebugProcessImpl.java index e9323cae6fa8..f7e63ad305c0 100644 --- a/java/debugger/impl/src/com/intellij/debugger/engine/DebugProcessImpl.java +++ b/java/debugger/impl/src/com/intellij/debugger/engine/DebugProcessImpl.java @@ -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 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 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 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 originalArgs, - @NotNull EvaluationContextImpl evaluationContext) throws EvaluateException { - DebugProcessImpl debugProcess = evaluationContext.getDebugProcess(); - ArrayList 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 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 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 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 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 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 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 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 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 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 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 invokeCommand = new InvokeCommand<>(method, args, (EvaluationContextImpl)evaluationContext) { - @Override - protected ObjectReference invokeMethod(ThreadReference thread, int invokePolicy, Method method, List 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 invokeCommand = new InvokeCommand<>(method, args, (EvaluationContextImpl)evaluationContext) { + @Override + protected ObjectReference invokeMethod(ThreadReference thread, int invokePolicy, Method method, List 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) { diff --git a/java/debugger/impl/src/com/intellij/debugger/engine/MethodInvokeUtils.kt b/java/debugger/impl/src/com/intellij/debugger/engine/MethodInvokeUtils.kt new file mode 100644 index 000000000000..55d7d77e6364 --- /dev/null +++ b/java/debugger/impl/src/com/intellij/debugger/engine/MethodInvokeUtils.kt @@ -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().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, + 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() + + 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?) + diff --git a/java/java-runtime/src/com/intellij/rt/debugger/ExceptionDebugHelper.java b/java/java-runtime/src/com/intellij/rt/debugger/ExceptionDebugHelper.java index 96435e5b4e3f..bfe2073d3ab4 100644 --- a/java/java-runtime/src/com/intellij/rt/debugger/ExceptionDebugHelper.java +++ b/java/java-runtime/src/com/intellij/rt/debugger/ExceptionDebugHelper.java @@ -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; - } } diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineStackFrameInterceptor.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineStackFrameInterceptor.kt index 794f31a16006..e521c7e3e510 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineStackFrameInterceptor.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineStackFrameInterceptor.kt @@ -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 }