From 9855b392a8517987ee26f21ad06dd3a7e37d8b36 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Sun, 13 Oct 2024 19:30:07 +0000 Subject: [PATCH] [debugger, devkit] IDEA-355621 Ignore PCE while stepping Merge-request: IJ-MR-145987 Merged-by: Maksim Zuev GitOrigin-RevId: 2270b4327c6719e95172b7bc5d969169e2b10636 --- .../debugger/impl/DebuggerUtilsImpl.java | 24 +++- .../internal/DebugAttachDetector.java | 19 +-- .../openapi/progress/Cancellation.java | 48 ++++++- .../util/DebugAttachDetectorArgs.java | 37 +++++ .../resources/intellij.devkit.debugger.xml | 9 ++ .../debugger/DebugeeIdeStateRenderer.kt | 49 ++++--- .../idea/devkit/debugger/stepping.kt | 127 ++++++++++++++++++ .../jetbrains/idea/devkit/debugger/utils.kt | 19 +++ ...t.kt => PlatformSupportClassesNameTest.kt} | 16 ++- .../SteppingUnderReadActionManualTest.kt | 79 +++++++++++ 10 files changed, 378 insertions(+), 49 deletions(-) create mode 100644 platform/util/src/com/intellij/util/DebugAttachDetectorArgs.java create mode 100644 plugins/devkit/intellij.devkit.debugger/src/org/jetbrains/idea/devkit/debugger/stepping.kt create mode 100644 plugins/devkit/intellij.devkit.debugger/src/org/jetbrains/idea/devkit/debugger/utils.kt rename plugins/devkit/intellij.devkit.debugger/test/org/jetbrains/idea/devkit/debugger/{ApplicationDebugSupportTest.kt => PlatformSupportClassesNameTest.kt} (55%) create mode 100644 plugins/devkit/intellij.devkit.debugger/test/org/jetbrains/idea/devkit/debugger/SteppingUnderReadActionManualTest.kt diff --git a/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerUtilsImpl.java b/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerUtilsImpl.java index a32f7b25f986..efbe9c5da904 100644 --- a/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerUtilsImpl.java +++ b/java/debugger/impl/src/com/intellij/debugger/impl/DebuggerUtilsImpl.java @@ -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, diff --git a/platform/platform-impl/src/com/intellij/internal/DebugAttachDetector.java b/platform/platform-impl/src/com/intellij/internal/DebugAttachDetector.java index de858e5db9e7..01fb076ac68e 100644 --- a/platform/platform-impl/src/com/intellij/internal/DebugAttachDetector.java +++ b/platform/platform-impl/src/com/intellij/internal/DebugAttachDetector.java @@ -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() { diff --git a/platform/util/src/com/intellij/openapi/progress/Cancellation.java b/platform/util/src/com/intellij/openapi/progress/Cancellation.java index 3d27ca6d7d45..f6ddec3b07cb 100644 --- a/platform/util/src/com/intellij/openapi/progress/Cancellation.java +++ b/platform/util/src/com/intellij/openapi/progress/Cancellation.java @@ -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 isInNonCancelableSection = new ThreadLocal<>(); + /** + * This flag is used only while debugging an IDE. + * + * @see Cancellation#initThreadNonCancellableState() + */ + private static final ThreadLocal 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 { * */ public static T computeInNonCancelableSection(@NotNull ThrowableComputable 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 forceNonCancellableSectionInClassInitializer(@NotNull Supplier 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; + } } diff --git a/platform/util/src/com/intellij/util/DebugAttachDetectorArgs.java b/platform/util/src/com/intellij/util/DebugAttachDetectorArgs.java new file mode 100644 index 000000000000..8d9eb69987e4 --- /dev/null +++ b/platform/util/src/com/intellij/util/DebugAttachDetectorArgs.java @@ -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; + } +} diff --git a/plugins/devkit/intellij.devkit.debugger/resources/intellij.devkit.debugger.xml b/plugins/devkit/intellij.devkit.debugger/resources/intellij.devkit.debugger.xml index 242362e1f4da..8e9864f01858 100644 --- a/plugins/devkit/intellij.devkit.debugger/resources/intellij.devkit.debugger.xml +++ b/plugins/devkit/intellij.devkit.debugger/resources/intellij.devkit.debugger.xml @@ -6,5 +6,14 @@ + + + + + + + \ No newline at end of file diff --git a/plugins/devkit/intellij.devkit.debugger/src/org/jetbrains/idea/devkit/debugger/DebugeeIdeStateRenderer.kt b/plugins/devkit/intellij.devkit.debugger/src/org/jetbrains/idea/devkit/debugger/DebugeeIdeStateRenderer.kt index 2a39ac637e32..589c2a08c814 100644 --- a/plugins/devkit/intellij.devkit.debugger/src/org/jetbrains/idea/devkit/debugger/DebugeeIdeStateRenderer.kt +++ b/plugins/devkit/intellij.devkit.debugger/src/org/jetbrains/idea/devkit/debugger/DebugeeIdeStateRenderer.kt @@ -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() +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()) 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): Pair { 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 diff --git a/plugins/devkit/intellij.devkit.debugger/src/org/jetbrains/idea/devkit/debugger/stepping.kt b/plugins/devkit/intellij.devkit.debugger/src/org/jetbrains/idea/devkit/debugger/stepping.kt new file mode 100644 index 000000000000..c07e32750f01 --- /dev/null +++ b/plugins/devkit/intellij.devkit.debugger/src/org/jetbrains/idea/devkit/debugger/stepping.kt @@ -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() + + 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() + + /** + * 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) { + 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) diff --git a/plugins/devkit/intellij.devkit.debugger/src/org/jetbrains/idea/devkit/debugger/utils.kt b/plugins/devkit/intellij.devkit.debugger/src/org/jetbrains/idea/devkit/debugger/utils.kt new file mode 100644 index 000000000000..3e74b3686f62 --- /dev/null +++ b/plugins/devkit/intellij.devkit.debugger/src/org/jetbrains/idea/devkit/debugger/utils.kt @@ -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 + } +} diff --git a/plugins/devkit/intellij.devkit.debugger/test/org/jetbrains/idea/devkit/debugger/ApplicationDebugSupportTest.kt b/plugins/devkit/intellij.devkit.debugger/test/org/jetbrains/idea/devkit/debugger/PlatformSupportClassesNameTest.kt similarity index 55% rename from plugins/devkit/intellij.devkit.debugger/test/org/jetbrains/idea/devkit/debugger/ApplicationDebugSupportTest.kt rename to plugins/devkit/intellij.devkit.debugger/test/org/jetbrains/idea/devkit/debugger/PlatformSupportClassesNameTest.kt index 50e5a691b420..2d6d7775879b 100644 --- a/plugins/devkit/intellij.devkit.debugger/test/org/jetbrains/idea/devkit/debugger/ApplicationDebugSupportTest.kt +++ b/plugins/devkit/intellij.devkit.debugger/test/org/jetbrains/idea/devkit/debugger/PlatformSupportClassesNameTest.kt @@ -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) + } } diff --git a/plugins/devkit/intellij.devkit.debugger/test/org/jetbrains/idea/devkit/debugger/SteppingUnderReadActionManualTest.kt b/plugins/devkit/intellij.devkit.debugger/test/org/jetbrains/idea/devkit/debugger/SteppingUnderReadActionManualTest.kt new file mode 100644 index 000000000000..859974c39f63 --- /dev/null +++ b/plugins/devkit/intellij.devkit.debugger/test/org/jetbrains/idea/devkit/debugger/SteppingUnderReadActionManualTest.kt @@ -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 { cancellingCode() }.executeSynchronously() + } + } + + private fun doTest(action: suspend () -> Unit) { + AppExecutorUtil.getAppExecutorService().submit { + runBlocking { + val channel = Channel(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 + } + } +}