mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
intellij-monorepo-bot
parent
36d0b02a0d
commit
9855b392a8
@@ -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>
|
||||
+23
-26
@@ -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
|
||||
|
||||
+127
@@ -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)
|
||||
+19
@@ -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
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
+79
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user