[debugger, devkit] IDEA-355621 Ignore PCE while stepping

Merge-request: IJ-MR-145987
Merged-by: Maksim Zuev <Maksim.Zuev@jetbrains.com>

GitOrigin-RevId: 2270b4327c6719e95172b7bc5d969169e2b10636
This commit is contained in:
Maksim Zuev
2024-10-13 19:30:07 +00:00
committed by intellij-monorepo-bot
parent 36d0b02a0d
commit 9855b392a8
10 changed files with 378 additions and 49 deletions
@@ -547,17 +547,31 @@ public final class DebuggerUtilsImpl extends DebuggerUtilsEx {
return new Range<>(variableImpl.getScopeStart(), variableImpl.getScopeEnd());
}
public static @Nullable Value invokeObjectMethod(@NotNull EvaluationContextImpl evaluationContext,
public static @Nullable Value invokeClassMethod(@NotNull EvaluationContext evaluationContext,
@NotNull ClassType type,
@NotNull String methodName,
@Nullable String signature) throws EvaluateException {
Method method = findMethodOrLogError(type, methodName, signature);
if (method == null) return null;
return evaluationContext.getDebugProcess().invokeMethod(evaluationContext, type, method, Collections.emptyList());
}
public static @Nullable Value invokeObjectMethod(@NotNull EvaluationContext evaluationContext,
@NotNull ObjectReference value,
@NotNull String methodName,
@Nullable String signature) throws EvaluateException {
ReferenceType type = value.referenceType();
Method method = findMethodOrLogError(type, methodName, signature);
if (method == null) return null;
return evaluationContext.getDebugProcess().invokeMethod(evaluationContext, value, method, Collections.emptyList());
}
private static @Nullable Method findMethodOrLogError(ReferenceType type, @NotNull String methodName, @Nullable String signature) {
Method method = findMethod(type, methodName, signature);
if (method != null) {
return evaluationContext.getDebugProcess().invokeMethod(evaluationContext, value, method, Collections.emptyList());
if (method == null) {
LOG.error("Method " + methodName + ", signature " + signature + " not found in class " + type.name());
}
LOG.error("Method " + methodName + ", signature " + signature + " not found in class " + type.name());
return null;
return method;
}
public static Value invokeHelperMethod(EvaluationContextImpl evaluationContext,
@@ -8,11 +8,10 @@ import com.intellij.notification.Notifications;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.util.DebugAttachDetectorArgs;
import com.intellij.util.concurrency.AppExecutorUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.management.ManagementFactory;
import java.lang.reflect.InvocationTargetException;
import java.util.Properties;
import java.util.concurrent.ScheduledFuture;
@@ -84,23 +83,13 @@ public final class DebugAttachDetector {
return property != null && property.isEmpty();
}
private static final @Nullable String DEBUG_ARGS = getDebugArgs();
private static @Nullable String getDebugArgs() {
for (String value : ManagementFactory.getRuntimeMXBean().getInputArguments()) {
if (value.contains("-agentlib:jdwp")) {
return value;
}
}
return null;
}
public static boolean isDebugEnabled() {
return DEBUG_ARGS != null;
return DebugAttachDetectorArgs.getDebugArgs() != null;
}
private static boolean isDebugServer() {
return DEBUG_ARGS != null && DEBUG_ARGS.contains("server=y");
String args = DebugAttachDetectorArgs.getDebugArgs();
return args != null && args.contains("server=y");
}
public static boolean isAttached() {
@@ -4,6 +4,7 @@ package com.intellij.openapi.progress;
import com.intellij.concurrency.ThreadContext;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.util.ThrowableComputable;
import com.intellij.util.DebugAttachDetectorArgs;
import kotlinx.coroutines.Job;
import kotlinx.coroutines.JobKt;
import org.jetbrains.annotations.ApiStatus.Internal;
@@ -65,7 +66,22 @@ public final class Cancellation {
// do not supply initial value to conserve memory
private static final ThreadLocal<Boolean> isInNonCancelableSection = new ThreadLocal<>();
/**
* This flag is used only while debugging an IDE.
*
* @see Cancellation#initThreadNonCancellableState()
*/
private static final ThreadLocal<DebugNonCancellableState> debugIsInNonCancelableSection = new ThreadLocal<>();
public static boolean isInNonCancelableSection() {
if (isInNonCancelableSectionInternal()) return true;
// Avoid thread-local access when the debugger is not enabled.
if (!DebugNonCancellableState.isDebugEnabled) return false;
DebugNonCancellableState state = debugIsInNonCancelableSection.get();
return state != null && state.inNonCancelableSection;
}
private static boolean isInNonCancelableSectionInternal() {
return isInNonCancelableSection.get() != null;
}
@@ -90,7 +106,7 @@ public final class Cancellation {
* </pre>
*/
public static <T, E extends Exception> T computeInNonCancelableSection(@NotNull ThrowableComputable<T, E> computable) throws E {
if (isInNonCancelableSection()) {
if (isInNonCancelableSectionInternal()) {
return computable.compute();
}
try {
@@ -116,7 +132,7 @@ public final class Cancellation {
}
public static @NotNull AccessToken withNonCancelableSection() {
if (isInNonCancelableSection()) {
if (isInNonCancelableSectionInternal()) {
return AccessToken.EMPTY_ACCESS_TOKEN;
}
@@ -148,4 +164,32 @@ public final class Cancellation {
public static <T> T forceNonCancellableSectionInClassInitializer(@NotNull Supplier<T> computable) {
return computeInNonCancelableSection(computable::get);
}
/**
* Used from devkit plugin while debugging an IDE to prevent PCE throwing during stepping.
* @return cancellability state of the current thread which can be adjusted by the debugger
*/
@SuppressWarnings("unused")
@NotNull
private static DebugNonCancellableState initThreadNonCancellableState() {
DebugNonCancellableState state = debugIsInNonCancelableSection.get();
if (state != null) return state;
state = new DebugNonCancellableState();
debugIsInNonCancelableSection.set(state);
return state;
}
/**
* This state is extracted to a separate class so that the fields can be modified by the debugger without the need of evaluation.
* Do not modify the names without the corresponding updates in the devkit plugin.
*/
private static class DebugNonCancellableState {
private static final boolean isDebugEnabled = DebugAttachDetectorArgs.isDebugEnabled();
/**
* This field is set to true only via debugger.
*/
@SuppressWarnings("FieldMayBeFinal")
private volatile boolean inNonCancelableSection = false;
}
}
@@ -0,0 +1,37 @@
// 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.util;
import com.intellij.openapi.diagnostic.Logger;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import java.lang.management.ManagementFactory;
/**
* Finds the debugger arguments passed to the IDE at startup.
*/
@ApiStatus.Internal
public final class DebugAttachDetectorArgs {
private static final @Nullable String DEBUG_ARGS = findDebugArgs();
private static @Nullable String findDebugArgs() {
try {
for (String value : ManagementFactory.getRuntimeMXBean().getInputArguments()) {
if (value.contains("-agentlib:jdwp")) {
return value;
}
}
} catch (Exception e) {
Logger.getInstance(DebugAttachDetectorArgs.class).error(e);
}
return null;
}
public static @Nullable String getDebugArgs() {
return DEBUG_ARGS;
}
public static boolean isDebugEnabled() {
return DEBUG_ARGS != null;
}
}
@@ -6,5 +6,14 @@
<extensions defaultExtensionNs="com.intellij">
<debugger.extraDebugNodesProvider implementation="org.jetbrains.idea.devkit.debugger.DebugeeIdeStateRenderer"/>
<registryKey defaultValue="true" description="Show details of a debugee IDE in the variables view" key="devkit.debugger.show.ide.state"/>
<registryKey defaultValue="false" description="Do not throw PCE during stepping" key="devkit.debugger.prevent.pce.while.stepping"/>
</extensions>
<applicationListeners>
<listener class="org.jetbrains.idea.devkit.debugger.SteppingStartListener"
topic="com.intellij.debugger.engine.SteppingListener"/>
</applicationListeners>
<projectListeners>
<listener class="org.jetbrains.idea.devkit.debugger.PauseListener"
topic="com.intellij.debugger.impl.DebuggerManagerListener"/>
</projectListeners>
</idea-plugin>
@@ -1,15 +1,11 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.debugger
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.DebuggerUtils
import com.intellij.debugger.engine.JavaStackFrame
import com.intellij.debugger.engine.JavaValue
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.*
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.engine.jdi.StackFrameProxy
import com.intellij.debugger.impl.DebuggerUtilsImpl
import com.intellij.debugger.memory.utils.StackFrameItem
import com.intellij.debugger.ui.tree.ExtraDebugNodesProvider
import com.intellij.icons.AllIcons
@@ -21,7 +17,7 @@ import com.intellij.xdebugger.impl.frame.XFramesView
import com.sun.jdi.BooleanValue
import com.sun.jdi.ClassType
import com.sun.jdi.ObjectReference
import com.sun.jdi.Value
import java.util.*
/**
* @see com.intellij.ide.debug.ApplicationStateDebugSupport
@@ -36,29 +32,30 @@ private const val APPLICATION_IMPL_FQN = "com.intellij.openapi.application.impl.
private const val COROUTINES_KT_FQN = "com.intellij.openapi.application.CoroutinesKt"
private const val ACTIONS_KT_FQN = "com.intellij.openapi.application.ActionsKt"
internal data class IdeState(val readAllowed: Boolean?, val writeAllowed: Boolean?)
private val cachedIdeState = WeakHashMap<SuspendContext, IdeState?>()
internal fun getIdeState(evaluationContext: EvaluationContext): IdeState? = cachedIdeState.computeIfAbsent(evaluationContext.suspendContext) f@{
val supportClass = findClassOrNull(evaluationContext, SUPPORT_CLASS_FQN) as? ClassType ?: return@f null
val state = DebuggerUtilsImpl.invokeClassMethod(evaluationContext, supportClass, GET_STATE_METHOD_NAME, GET_STATE_METHOD_SIGNATURE)
as? ObjectReference ?: return@f null
val stateClass = state.referenceType()
val fieldValues = state.getValues(stateClass.allFields()).mapKeys { it.key.name() }
val readField = (fieldValues[READ_ACTION_ALLOWED_FIELD_NAME] as? BooleanValue)?.value()
val writeField = (fieldValues[WRITE_ACTION_ALLOWED_FIELD_NAME] as? BooleanValue)?.value()
IdeState(readAllowed = readField, writeAllowed = writeField)
}
internal class DebugeeIdeStateRenderer : ExtraDebugNodesProvider {
override fun addExtraNodes(evaluationContext: EvaluationContext, children: XValueChildrenList) {
if (!Registry.`is`("devkit.debugger.show.ide.state")) return
val debugProcess = evaluationContext.debugProcess as? DebugProcessImpl ?: return
val supportClass = try {
debugProcess.findLoadedClass(evaluationContext, SUPPORT_CLASS_FQN, evaluationContext.classLoader) ?: return
}
catch (_: EvaluateException) {
return
}
val getStateMethod = DebuggerUtils.findMethod(supportClass, GET_STATE_METHOD_NAME, GET_STATE_METHOD_SIGNATURE) ?: return
val state = debugProcess.invokeMethod(evaluationContext, supportClass as ClassType, getStateMethod, emptyList<Value>()) as ObjectReference?
if (state == null) return
val stateClass = state.referenceType()
val ideState = getIdeState(evaluationContext) ?: return
if (ideState.readAllowed == null && ideState.writeAllowed == null) return
val fieldValues = state.getValues(stateClass.allFields()).mapKeys { it.key.name() }
val readField = (fieldValues[READ_ACTION_ALLOWED_FIELD_NAME] as? BooleanValue)?.value()
val writeField = (fieldValues[WRITE_ACTION_ALLOWED_FIELD_NAME] as? BooleanValue)?.value()
if (readField == null && writeField == null) return
val (isReadActionAllowed, isWriteActionAllowed) = (readField to writeField).adjustLockStatus(evaluationContext)
val (isReadActionAllowed, isWriteActionAllowed) = (ideState.readAllowed to ideState.writeAllowed).adjustLockStatus(evaluationContext)
fun icon(isAvailable: Boolean) = if (isAvailable) "✓" else "✗"
children.addTopValue(object : XNamedValue(DevKitDebuggerBundle.message("debugger.ide.state")) {
@@ -159,7 +156,7 @@ private fun findLockAccessIndex(frames: List<StackFrameProxy>): Pair<Int, Int> {
if (className == THREADING_SUPPORT_FQN
&& (methodName == "runWriteAction"
&& signature == "(Ljava/lang/Class;Lcom/intellij/openapi/util/ThrowableComputable;)Ljava/lang/Object;")) {
writeIndex = i;
writeIndex = i
}
}
if (readIndex != -1 && writeIndex != -1) break
@@ -0,0 +1,127 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.debugger
import com.intellij.debugger.engine.*
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.impl.DebuggerManagerListener
import com.intellij.debugger.impl.DebuggerSession
import com.intellij.debugger.impl.DebuggerUtilsImpl
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
import com.intellij.openapi.Disposable
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.registry.Registry
import com.sun.jdi.BooleanValue
import com.sun.jdi.ClassType
import com.sun.jdi.ObjectReference
import java.util.concurrent.ConcurrentHashMap
private object PauseListener : DebuggerManagerListener {
private val sessions = ConcurrentHashMap<DebuggerSession, SessionThreadsData>()
override fun sessionAttached(session: DebuggerSession?) {
if (session == null) return
val disposable = Disposer.newDisposable()
sessions[session] = SessionThreadsData(disposable)
session.process.addDebugProcessListener(object : DebugProcessListener {
override fun paused(suspendContext: SuspendContext) {
val context = suspendContext as? SuspendContextImpl ?: return
val pausedThreads = context.debugProcess.suspendManager.pausedContexts.mapNotNull { it.thread }
getSessionData(context.debugProcess.session)?.resetNonCancellableSection(context, pausedThreads)
}
}, disposable)
}
override fun sessionDetached(session: DebuggerSession?) {
if (session == null) return
val sessionData = sessions.remove(session) ?: return
Disposer.dispose(sessionData.disposable)
}
fun getSessionData(session: DebuggerSession): SessionThreadsData? = sessions[session]
}
private class SteppingStartListener : SteppingListener {
override fun beforeSteppingStarted(suspendContext: SuspendContextImpl, steppingAction: SteppingAction) {
PauseListener.getSessionData(suspendContext.debugProcess.session)?.setNonCancellableSection(suspendContext)
}
}
private data class ThreadState(val reference: ObjectReference, var state: Boolean = false) {
/**
* @see com.intellij.openapi.progress.Cancellation.DebugNonCancellableState.inNonCancelableSection
*/
fun setNonCancellable(suspendContext: SuspendContextImpl, value: Boolean) {
if (value == state) return
state = value
val field = (reference.type() as ClassType).fieldByName("inNonCancelableSection") ?: return
reference.setValue(field, booleanValue(suspendContext, value))
}
}
/**
* Manages cancellability state of the IDE threads within a single debugger session.
*/
private class SessionThreadsData(val disposable: Disposable) {
private val threadStates = hashMapOf<ThreadReferenceProxyImpl, ThreadState?>()
/**
* Sets the non-cancellable state for the current thread.
* This method requires a suspend context command, as it may cause evaluation.
*/
fun setNonCancellableSection(suspendContext: SuspendContextImpl) {
if (!isSteppingAdjustmentEnabled()) return
try {
val state = getOrCreateThreadState(suspendContext) ?: return
state.setNonCancellable(suspendContext, true)
}
catch (e: Exception) {
thisLogger().error(e)
}
}
/**
* Resets the non-cancellable flag for the paused threads.
*/
fun resetNonCancellableSection(suspendContext: SuspendContextImpl, pausedThreads: List<ThreadReferenceProxyImpl>) {
if (!isSteppingAdjustmentEnabled()) return
try {
for (state in pausedThreads.mapNotNull { threadStates[it] }) {
state.setNonCancellable(suspendContext, false)
}
}
catch (e: Exception) {
thisLogger().error(e)
}
}
/**
* Get a reference to the [com.intellij.openapi.progress.Cancellation.DebugNonCancellableState] instance
* bounded to the current thread.
* Uses cached value if already created to reduce the number of evaluations.
*/
private fun getOrCreateThreadState(suspendContext: SuspendContextImpl): ThreadState? {
val thread = suspendContext.thread ?: return null
if (threadStates.containsKey(thread)) return threadStates[thread]
val reference = initializeThreadState(suspendContext)
val state = reference?.let { ThreadState(it) }
return state.also { threadStates[thread] = it }
}
}
private fun isSteppingAdjustmentEnabled() = Registry.`is`("devkit.debugger.prevent.pce.while.stepping")
/**
* @see com.intellij.openapi.progress.Cancellation.initThreadNonCancellableState
* @see com.intellij.openapi.progress.Cancellation.isInNonCancelableSection
*/
private fun initializeThreadState(suspendContext: SuspendContextImpl): ObjectReference? {
val evaluationContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy)
val cancellationClass = findClassOrNull(evaluationContext, "com.intellij.openapi.progress.Cancellation") as? ClassType ?: return null
return DebuggerUtilsImpl.invokeClassMethod(evaluationContext, cancellationClass, "initThreadNonCancellableState",
"()Lcom/intellij/openapi/progress/Cancellation\$DebugNonCancellableState;") as? ObjectReference
}
private fun booleanValue(suspendContext: SuspendContextImpl, b: Boolean): BooleanValue = suspendContext.virtualMachineProxy.mirrorOf(b)
@@ -0,0 +1,19 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.debugger
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.SuspendContextImpl
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
import com.sun.jdi.ReferenceType
internal fun findClassOrNull(evaluationContext: EvaluationContext, fqn: String): ReferenceType? {
val debugProcess = evaluationContext.debugProcess as? DebugProcessImpl ?: return null
return try {
debugProcess.findLoadedClass(evaluationContext, fqn, evaluationContext.classLoader)
}
catch (_: EvaluateException) {
null
}
}
@@ -4,7 +4,7 @@ package org.jetbrains.idea.devkit.debugger
import com.intellij.testFramework.HeavyPlatformTestCase
import org.junit.Assert
class ApplicationDebugSupportTest : HeavyPlatformTestCase() {
class PlatformSupportClassesNameTest : HeavyPlatformTestCase() {
fun testApplicationDebugSupportMethodExists() {
val clazz = Class.forName("com.intellij.ide.debug.ApplicationStateDebugSupport")
Assert.assertNotNull(clazz.getMethod("getApplicationState"))
@@ -18,4 +18,18 @@ class ApplicationDebugSupportTest : HeavyPlatformTestCase() {
boolean writeActionAllowed
""".trimIndent(), fields)
}
fun testCancellationMethodExists() {
val clazz = Class.forName("com.intellij.openapi.progress.Cancellation")
Assert.assertNotNull(clazz.getDeclaredMethod("initThreadNonCancellableState"))
}
fun testCancellationStatusFields() {
val clazz = Class.forName("com.intellij.openapi.progress.Cancellation\$DebugNonCancellableState")
val fields = clazz.declaredFields.joinToString("\n") { "${it.type} ${it.name}" }
assertEquals("""
boolean isDebugEnabled
boolean inNonCancelableSection
""".trimIndent(), fields)
}
}
@@ -0,0 +1,79 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.debugger
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressManager
import com.intellij.testFramework.HeavyPlatformTestCase
import com.intellij.util.concurrency.AppExecutorUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@Ignore("Manually run only")
@RunWith(JUnit4::class)
class SteppingUnderReadActionManualTest : HeavyPlatformTestCase() {
@Test
fun testCoroutineReadActionStepping() {
doTest {
readAction {
cancellingCode()
}
}
}
@Test
fun testReadActionStepping() {
doTest {
ReadAction.nonBlocking<Unit> { cancellingCode() }.executeSynchronously()
}
}
private fun doTest(action: suspend () -> Unit) {
AppExecutorUtil.getAppExecutorService().submit {
runBlocking {
val channel = Channel<Unit>(Channel.UNLIMITED)
val writeJob = launch(Dispatchers.Default) {
var counter = 0
while (true) {
writeAction {
println("Write action ${counter++}")
}
channel.send(Unit)
}
}
repeat(10) {
channel.receive()
while (channel.tryReceive().isSuccess) {
}
action()
}
writeJob.cancel()
}
}
}
private fun cancellingCode() {
println("ReadAction")
ProgressManager.checkCanceled()
try {
ProgressManager.checkCanceled()
Thread.sleep(1)
ProgressManager.checkCanceled()
ProgressManager.checkCanceled()
ProgressManager.checkCanceled()
}
catch (e: ProcessCanceledException) {
println("Read action cancelled")
throw e
}
}
}