diff --git a/idea/idea-analysis/resources/org/jetbrains/kotlin/idea/KotlinBundle.properties b/idea/idea-analysis/resources/org/jetbrains/kotlin/idea/KotlinBundle.properties index 1a3bab65d9c..c7856a37ca7 100644 --- a/idea/idea-analysis/resources/org/jetbrains/kotlin/idea/KotlinBundle.properties +++ b/idea/idea-analysis/resources/org/jetbrains/kotlin/idea/KotlinBundle.properties @@ -254,6 +254,13 @@ debugger.field.watchpoints.properties.panel.field.access.label=Field &access debugger.field.watchpoints.properties.panel.field.modification.label=Field &modification debugger.field.watchpoints.properties.panel.field.initialization.label=Field &initialization +# Coroutine Debugger +debugger.session.tab.coroutine.title=Coroutines +debugger.session.tab.coroutine.message.failure=Failed to retrieve coroutine status +debugger.session.tab.coroutine.message.resume=Application resumed +debugger.session.tab.coroutine.message.error=Coroutine dump failed, see log +debugger.session.tab.xcoroutine.title=XCoroutines + # Android Lint android.klint.inspections.group.name=Android Lint for Kotlin diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleCoroutineDebugProjectResolver.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleCoroutineDebugProjectResolver.kt index b77ae18d7dd..4066771220b 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleCoroutineDebugProjectResolver.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleCoroutineDebugProjectResolver.kt @@ -28,7 +28,7 @@ class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtensi val lines = arrayOf( "gradle.taskGraph.beforeTask { Task task ->", " if (task instanceof Test) {", - " def kotlinxCoroutinesDebugJar = task.classpath.find { it.name.contains(\"kotlinx-coroutines-debug\") }", + " def kotlinxCoroutinesDebugJar = task.classpath.find { it.name.startsWith(\"kotlinx-coroutines-debug\") }", " if (kotlinxCoroutinesDebugJar)", " task.jvmArgs (\"-javaagent:\${kotlinxCoroutinesDebugJar?.absolutePath}\", \"-ea\")", " }", @@ -38,5 +38,6 @@ class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtensi initScriptConsumer.consume(script) } + // supposed to be the same as [CoroutineProjectConnectionListener.kt].coroutineDebuggerEnabled private fun coroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines") } \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/AsyncStackTraceContext.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/AsyncStackTraceContext.kt deleted file mode 100644 index 85547008244..00000000000 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/AsyncStackTraceContext.kt +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -package org.jetbrains.kotlin.idea.debugger - -import com.intellij.debugger.DebuggerContext -import com.intellij.debugger.engine.JavaValue -import com.intellij.debugger.engine.evaluation.EvaluateException -import com.intellij.debugger.engine.evaluation.EvaluationContextImpl -import com.intellij.debugger.jdi.GeneratedLocation -import com.intellij.debugger.memory.utils.StackFrameItem -import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl -import com.intellij.xdebugger.frame.XNamedValue -import com.sun.jdi.* -import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_VARIABLE_NAME -import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext - -class AsyncStackTraceContext( - val context: ExecutionContext, - val method: Method, - private val debugMetadataKtType: ClassType -) { - - fun getAsyncStackTraceForSuspendLambda(): List? { - if (method.name() != "invokeSuspend" || method.signature() != "(Ljava/lang/Object;)Ljava/lang/Object;") { - return null - } - - val thisObject = context.frameProxy.thisObject() ?: return null - val thisType = thisObject.referenceType() - - if (SUSPEND_LAMBDA_CLASSES.none { thisType.isSubtype(it) }) { - return null - } - - return collectFrames(thisObject) - } - - fun getAsyncStackTraceForSuspendFunction(): List? { - if ("Lkotlin/coroutines/Continuation;)" !in method.signature()) { - return null - } - - val frameProxy = context.frameProxy - val continuationVariable = frameProxy.safeVisibleVariableByName(CONTINUATION_VARIABLE_NAME) ?: return null - val continuation = frameProxy.getValue(continuationVariable) as? ObjectReference ?: return null - context.keepReference(continuation) - - return collectFrames(continuation) - } - - private fun collectFrames(continuation: ObjectReference): List? { - val frames = mutableListOf() - collectFramesRecursively(continuation, frames) - return frames - } - - private fun collectFramesRecursively(continuation: ObjectReference, consumer: MutableList) { - val continuationType = continuation.referenceType() as? ClassType ?: return - val baseContinuationSupertype = findBaseContinuationSuperSupertype(continuationType) ?: return - - val location = getLocation(continuation) - val spilledVariables = getSpilledVariables(continuation) ?: emptyList() - - if (location != null) { - consumer += StackFrameItem(location, spilledVariables) - } - - val completionField = baseContinuationSupertype.fieldByName("completion") ?: return - val completion = continuation.getValue(completionField) as? ObjectReference ?: return - collectFramesRecursively(completion, consumer) - } - - private fun getLocation(continuation: ObjectReference): Location? { - val getStackTraceElementMethod = debugMetadataKtType.methodsByName( - "getStackTraceElement", - "(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)Ljava/lang/StackTraceElement;" - ).firstOrNull() ?: return null - - val args = listOf(continuation) - - val stackTraceElement = context.invokeMethod(debugMetadataKtType, getStackTraceElementMethod, args) as? ObjectReference - ?: return null - - context.keepReference(stackTraceElement) - - val stackTraceElementType = stackTraceElement.referenceType().takeIf { it.name() == StackTraceElement::class.java.name } - ?: return null - - fun getValue(name: String, desc: String): Value? { - val method = stackTraceElementType.methodsByName(name, desc).single() - return context.invokeMethod(stackTraceElement, method, emptyList()) - } - - val className = (getValue("getClassName", "()Ljava/lang/String;") as? StringReference)?.value() ?: return null - val methodName = (getValue("getMethodName", "()Ljava/lang/String;") as? StringReference)?.value() ?: return null - val lineNumber = (getValue("getLineNumber", "()I") as? IntegerValue)?.value()?.takeIf { it >= 0 } ?: return null - - val locationClass = context.findClassSafe(className) ?: return null - return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber) - } - - fun getSpilledVariables(continuation: ObjectReference): List? { - val getSpilledVariableFieldMappingMethod = debugMetadataKtType.methodsByName( - "getSpilledVariableFieldMapping", - "(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;" - ).firstOrNull() ?: return null - - val args = listOf(continuation) - - val rawSpilledVariables = context.invokeMethod(debugMetadataKtType, getSpilledVariableFieldMappingMethod, args) as? ArrayReference - ?: return null - - context.keepReference(rawSpilledVariables) - - val length = rawSpilledVariables.length() / 2 - val spilledVariables = ArrayList(length) - - for (index in 0 until length) { - val fieldName = (rawSpilledVariables.getValue(2 * index) as? StringReference)?.value() ?: continue - val variableName = (rawSpilledVariables.getValue(2 * index + 1) as? StringReference)?.value() ?: continue - val field = continuation.referenceType().fieldByName(fieldName) ?: continue - - val valueDescriptor = object : ValueDescriptorImpl(context.project) { - override fun calcValueName() = variableName - override fun calcValue(evaluationContext: EvaluationContextImpl?) = continuation.getValue(field) - override fun getDescriptorEvaluation(context: DebuggerContext?) = - throw EvaluateException("Spilled variable evaluation is not supported") - } - - spilledVariables += JavaValue.create( - null, - valueDescriptor, - context.evaluationContext, - context.debugProcess.xdebugProcess!!.nodeManager, - false - ) - } - - return spilledVariables - } - - private tailrec fun findBaseContinuationSuperSupertype(type: ClassType): ClassType? { - if (type.name() == "kotlin.coroutines.jvm.internal.BaseContinuationImpl") { - return type - } - return findBaseContinuationSuperSupertype(type.superclass() ?: return null) - } -} - diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProvider.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProvider.kt deleted file mode 100644 index b70a1ccf4f3..00000000000 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProvider.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -package org.jetbrains.kotlin.idea.debugger - -import com.intellij.debugger.engine.JavaStackFrame -import com.intellij.debugger.engine.SuspendContextImpl -import com.intellij.debugger.engine.evaluation.EvaluationContextImpl -import com.intellij.debugger.jdi.StackFrameProxyImpl -import com.intellij.debugger.memory.utils.StackFrameItem -import com.sun.jdi.* -import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext - -class KotlinCoroutinesAsyncStackTraceProvider : KotlinCoroutinesAsyncStackTraceProviderBase { - private companion object { - const val DEBUG_METADATA_KT = "kotlin.coroutines.jvm.internal.DebugMetadataKt" - } - - - override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List? { - return hopelessAware { getAsyncStackTraceSafe(stackFrame.stackFrameProxy, suspendContext) } - } - - fun getAsyncStackTraceSafe(frameProxy: StackFrameProxyImpl, suspendContext: SuspendContextImpl): List? { - val location = frameProxy.location() - if (!location.isInKotlinSources()) { - return null - } - - val method = location.safeMethod() ?: return null - val threadReference = frameProxy.threadProxy().threadReference - - if (threadReference == null || !threadReference.isSuspended || !suspendContext.debugProcess.canRunEvaluation) { - return null - } - - val context = createExecutionContext(suspendContext, frameProxy) - - // DebugMetadataKt not found, probably old kotlin-stdlib version - val debugMetadataKtType = findDebugMetadata(context) ?: return null - - val asyncContext = AsyncStackTraceContext(context, method, debugMetadataKtType) - return asyncContext.getAsyncStackTraceForSuspendLambda() ?: asyncContext.getAsyncStackTraceForSuspendFunction() - } - - private fun findDebugMetadata(context: ExecutionContext): ClassType? = context.findClassSafe(DEBUG_METADATA_KT) - - private fun createExecutionContext( - suspendContext: SuspendContextImpl, - frameProxy: StackFrameProxyImpl - ): ExecutionContext { - val evaluationContext = EvaluationContextImpl(suspendContext, frameProxy) - return ExecutionContext(evaluationContext, frameProxy) - } -} - -internal fun ExecutionContext.findClassSafe(className: String): ClassType? { - return try { - findClass(className) as? ClassType - } catch (e: Throwable) { - null - } -} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProviderBase.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProviderBase.kt deleted file mode 100644 index c2c0fe70817..00000000000 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/KotlinCoroutinesAsyncStackTraceProviderBase.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -package org.jetbrains.kotlin.idea.debugger - -import com.intellij.debugger.engine.AsyncStackTraceProvider -import com.intellij.debugger.engine.JavaStackFrame -import com.intellij.debugger.engine.SuspendContextImpl -import com.intellij.debugger.memory.utils.StackFrameItem - -interface KotlinCoroutinesAsyncStackTraceProviderBase : AsyncStackTraceProvider { - override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List? -} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/AsyncStackTraceContext.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/AsyncStackTraceContext.kt new file mode 100644 index 00000000000..8924e7b566f --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/AsyncStackTraceContext.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.intellij.debugger.DebuggerContext +import com.intellij.debugger.engine.JavaValue +import com.intellij.debugger.engine.evaluation.EvaluateException +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.jdi.GeneratedLocation +import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl +import com.intellij.xdebugger.frame.XNamedValue +import com.sun.jdi.* +import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_VARIABLE_NAME +import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext +import org.jetbrains.kotlin.idea.debugger.coroutines.util.logger +import org.jetbrains.kotlin.idea.debugger.SUSPEND_LAMBDA_CLASSES +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineAsyncStackFrameItem +import org.jetbrains.kotlin.idea.debugger.isSubtype +import org.jetbrains.kotlin.idea.debugger.safeVisibleVariableByName + +class AsyncStackTraceContext( + val context: ExecutionContext, + val method: Method) { + val log by logger + val debugMetadataKtType = context.findClassSafe( + DEBUG_METADATA_KT + )!! + + private companion object { + const val DEBUG_METADATA_KT = "kotlin.coroutines.jvm.internal.DebugMetadataKt" + } + + fun getAsyncStackTraceIfAny() : List { + val continuation = locateContinuation() ?: return emptyList() + val frames = mutableListOf() + collectFramesRecursively(continuation, frames) + return frames; + } + + private fun locateContinuation() : ObjectReference? { + val continuation : ObjectReference? + if (isInvokeSuspendMethod(method)) { + continuation = context.frameProxy.thisObject() ?: return null + if (!isSuspendLambda(continuation.referenceType())) + return null + } else if (isContinuationProvider(method)) { + val frameProxy = context.frameProxy + val continuationVariable = frameProxy.safeVisibleVariableByName(CONTINUATION_VARIABLE_NAME) ?: return null + continuation = frameProxy.getValue(continuationVariable) as? ObjectReference ?: return null + context.keepReference(continuation) + } else { + continuation = null + } + return continuation + } + + private fun isInvokeSuspendMethod(method: Method): Boolean = + method.name() == "invokeSuspend" && method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;" + + private fun isContinuationProvider(method: Method): Boolean = + "Lkotlin/coroutines/Continuation;)" in method.signature() + + private fun isSuspendLambda(referenceType: ReferenceType): Boolean = + SUSPEND_LAMBDA_CLASSES.any { referenceType.isSubtype(it) } + + private fun collectFramesRecursively(continuation: ObjectReference, consumer: MutableList) { + val continuationType = continuation.referenceType() as? ClassType ?: return + val baseContinuationSupertype = findBaseContinuationSuperSupertype(continuationType) ?: return + + val location = createLocation(continuation) + val spilledVariables = getSpilledVariables(continuation) ?: emptyList() + + location?.let { + consumer.add(CoroutineAsyncStackFrameItem(location, spilledVariables)) + } + + val completionField = baseContinuationSupertype.fieldByName("completion") ?: return + val completion = continuation.getValue(completionField) as? ObjectReference ?: return + collectFramesRecursively(completion, consumer) + } + + private fun createLocation(continuation: ObjectReference) : GeneratedLocation? { + val instance = invokeGetStackTraceElement(continuation) ?: return null + val className = context.invokeMethodAsString(instance, "getClassName") ?: return null + val methodName = context.invokeMethodAsString(instance, "getMethodName") ?: return null + val lineNumber = context.invokeMethodAsInt(instance,"getLineNumber") ?: return null + val locationClass = context.findClassSafe(className) ?: return null + log.warn("Got location of ${className}.${methodName}:${lineNumber} in ${locationClass}") + return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber) + } + + private fun invokeGetStackTraceElement(continuation: ObjectReference): ObjectReference? { + val stackTraceElement = + context.invokeMethodAsObject(debugMetadataKtType, "getStackTraceElement", continuation) ?: return null + // redundant i believe + stackTraceElement.referenceType().takeIf { it.name() == StackTraceElement::class.java.name } ?: return null + + context.keepReference(stackTraceElement) + return stackTraceElement + } + + fun getSpilledVariables(continuation: ObjectReference): List? { + + val rawSpilledVariables = + context.invokeMethodAsArray(debugMetadataKtType, "getSpilledVariableFieldMapping", "(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;", continuation) as? ArrayReference ?: return null + + context.keepReference(rawSpilledVariables) + + val length = rawSpilledVariables.length() / 2 + val spilledVariables = ArrayList(length) + + for (index in 0 until length) { + val fieldName = (rawSpilledVariables.getValue(2 * index) as? StringReference)?.value() ?: continue + val variableName = (rawSpilledVariables.getValue(2 * index + 1) as? StringReference)?.value() ?: continue + val field = continuation.referenceType().fieldByName(fieldName) ?: continue + + val valueDescriptor = object : ValueDescriptorImpl(context.project) { + override fun calcValueName() = variableName + override fun calcValue(evaluationContext: EvaluationContextImpl?) = continuation.getValue(field) + override fun getDescriptorEvaluation(context: DebuggerContext?) = + throw EvaluateException("Spilled variable evaluation is not supported") + } + + spilledVariables += JavaValue.create( + null, + valueDescriptor, + context.evaluationContext, + context.debugProcess.xdebugProcess!!.nodeManager, + false + ) + } + + return spilledVariables + } + + private tailrec fun findBaseContinuationSuperSupertype(type: ClassType): ClassType? { + if (type.name() == "kotlin.coroutines.jvm.internal.BaseContinuationImpl") { + return type + } + return findBaseContinuationSuperSupertype(type.superclass() ?: return null) + } +} + diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineAsyncStackTraceProvider.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineAsyncStackTraceProvider.kt new file mode 100644 index 00000000000..dca08cc682e --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineAsyncStackTraceProvider.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.intellij.debugger.engine.* +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.debugger.memory.utils.StackFrameItem +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.* +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineAsyncStackFrameItem +import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext + +class CoroutineAsyncStackTraceProvider : AsyncStackTraceProvider { + + override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List? { + val stackFrameList = hopelessAware { getAsyncStackTraceSafe(stackFrame.stackFrameProxy, suspendContext) } ?: emptyList() + return null + } + + fun getAsyncStackTraceSafe(frameProxy: StackFrameProxyImpl, suspendContext: SuspendContext): List { + val defaultResult = emptyList() + + val location = frameProxy.location() + if (!location.isInKotlinSources()) + return defaultResult + + val method = location.safeMethod() ?: return defaultResult + val threadReference = frameProxy.threadProxy().threadReference + + if (threadReference == null || !threadReference.isSuspended || !(suspendContext.debugProcess as DebugProcessImpl).canRunEvaluation) + return defaultResult + + + val astContext = createAsyncStackTraceContext(frameProxy, suspendContext, method) + return astContext.getAsyncStackTraceIfAny() + } + + private fun createAsyncStackTraceContext( + frameProxy: StackFrameProxyImpl, + suspendContext: SuspendContext, + method: Method + ): AsyncStackTraceContext { + val evaluationContext = EvaluationContextImpl(suspendContext as SuspendContextImpl, frameProxy) + val context = ExecutionContext(evaluationContext, frameProxy) + // DebugMetadataKt not found, probably old kotlin-stdlib version + return AsyncStackTraceContext(context, method) + } +} + diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDebuggerContentInfo.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDebuggerContentInfo.kt new file mode 100644 index 00000000000..6762af5e434 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDebuggerContentInfo.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines + +import org.jetbrains.annotations.NonNls + +class CoroutineDebuggerContentInfo { + companion object { + val COROUTINE_THREADS_CONTENT = "CoroutineThreadsContent" + val XCOROUTINE_THREADS_CONTENT = "XCoroutineThreadsContent" + val XCOROUTINE_POPUP_ACTION_GROUP = "Kotlin.XDebugger.Actions" + } +} + +class CoroutineDebuggerActions { + companion object { + @NonNls + val COROUTINE_PANEL_POPUP: String = "Debugger.CoroutinesPanelPopup" + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineProjectConnectionListener.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineProjectConnectionListener.kt new file mode 100644 index 00000000000..b72cd165563 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineProjectConnectionListener.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines + +import com.intellij.debugger.DebuggerInvocationUtil +import com.intellij.debugger.actions.ThreadDumpAction +import com.intellij.debugger.engine.JavaDebugProcess +import com.intellij.debugger.impl.DebuggerSession +import com.intellij.execution.configurations.JavaParameters +import com.intellij.execution.configurations.RunConfigurationBase +import com.intellij.execution.configurations.RunnerSettings +import com.intellij.execution.ui.RunnerLayoutUi +import com.intellij.execution.ui.layout.PlaceInGrid +import com.intellij.execution.ui.layout.impl.RunnerContentUi +import com.intellij.execution.ui.layout.impl.RunnerLayoutUiImpl +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.registry.Registry +import com.intellij.ui.OnePixelSplitter +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.content.Content +import com.intellij.ui.content.ContentManagerAdapter +import com.intellij.ui.content.ContentManagerEvent +import com.intellij.util.messages.MessageBusConnection +import com.intellij.xdebugger.* +import com.intellij.xdebugger.impl.XDebugSessionImpl +import org.jetbrains.kotlin.idea.KotlinBundle +import org.jetbrains.kotlin.idea.debugger.coroutines.util.* +import org.jetbrains.kotlin.idea.debugger.coroutines.view.CoroutinesPanel +import org.jetbrains.kotlin.idea.debugger.coroutines.view.XCoroutineView +import java.util.concurrent.atomic.AtomicInteger +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + + +class CoroutineProjectConnectionListener(val project: Project) : XDebuggerManagerListener { + var connection: MessageBusConnection? = null + val processCounter = AtomicInteger(0) + private val log by logger + + private fun connect() { + connection = project.messageBus.connect() + connection?.subscribe(XDebuggerManager.TOPIC, this) + } + + fun configurationStarting( + configuration: RunConfigurationBase<*>, + params: JavaParameters?, + runnerSettings: RunnerSettings? + ) { + val configurationName = configuration.type.id + try { + if (!gradleConfiguration(configurationName)) { // gradle test logic in KotlinGradleCoroutineDebugProjectResolver + val kotlinxCoroutinesClassPathLib = + params?.classPath?.pathList?.first { it.contains("kotlinx-coroutines-debug") } + initializeCoroutineAgent(params!!, kotlinxCoroutinesClassPathLib) + } + starting() + } catch (e: NoSuchElementException) { + log.warn("'kotlinx-coroutines-debug' not found in classpath. Coroutine debugger disabled.") + } + } + + private fun starting() { + if (processCounter.compareAndSet(0, 1)) + connect() + else + processCounter.incrementAndGet() + } + + private fun gradleConfiguration(configurationName: String) = + "GradleRunConfiguration".equals(configurationName) || "KotlinGradleRunConfiguration".equals(configurationName) + + override fun processStarted(debugProcess: XDebugProcess) = + DebuggerInvocationUtil.swingInvokeLater(project) { + if (debugProcess is JavaDebugProcess) + registerCoroutinesPanel(debugProcess.session, debugProcess.debuggerSession) + } + + override fun processStopped(debugProcess: XDebugProcess) { + if (processCounter.compareAndSet(1, 0)) { + connection?.disconnect() + connection = null + } else + processCounter.decrementAndGet() + } + + private fun createThreadsContent(session: XDebugSession) { + val ui = session.ui ?: return + val xCoroutineThreadView = XCoroutineView(project, session as XDebugSessionImpl) + val framesContent: Content = createContent(ui, xCoroutineThreadView) + framesContent.isCloseable = false + ui.addContent(framesContent, 0, PlaceInGrid.right, false) + session.addSessionListener(xCoroutineThreadView.debugSessionListener(session)) + session.rebuildViews() + } + + private fun createContent(ui: RunnerLayoutUi, createContentParamProvider: CreateContentParamsProvider): Content { + val param = createContentParamProvider.createContentParams() + return ui.createContent(param.id, param.component, param.displayName, param.icon, param.parentComponent) + } + + /** + * Adds panel to XDebugSessionTab + */ + private fun registerCoroutinesPanel(session: XDebugSession, debuggerSession: DebuggerSession): Boolean { + val ui = session.ui ?: return false + val panel = CoroutinesPanel(project, debuggerSession.contextManager) + // evaluation of `debuggerSession.contextManager.toString()` leads to + // java.lang.Throwable: Assertion failed: Should be invoked in manager thread, use DebuggerManagerThreadImpl.getInstance(..).invoke + // as toString() is not allowed here + createThreadsContent(session) + + val content = ui.createContent( + CoroutineDebuggerContentInfo.COROUTINE_THREADS_CONTENT, + panel, + KotlinBundle.message("debugger.session.tab.coroutine.title"), + AllIcons.Debugger.ThreadGroup, + panel) + content.isCloseable = false + ui.addContent(content, 0, PlaceInGrid.left, true) + ui.addListener(object : ContentManagerAdapter() { + override fun selectionChanged(event: ContentManagerEvent) { + if (event.content === content) { + if (content.isSelected) { + panel.setUpdateEnabled(true) + if (panel.isRefreshNeeded) { + panel.rebuildIfVisible(DebuggerSession.Event.CONTEXT) + } + } else { + panel.setUpdateEnabled(false) + } + } + } + }, content) + // add coroutine dump button: due to api problem left toolbar is copied, modified and reset to tab + val runnerContent = (ui.options as RunnerLayoutUiImpl).getData(RunnerContentUi.KEY.name) as RunnerContentUi + val modifiedActions = runnerContent.getActions(true) + val pos = modifiedActions.indexOfLast { it is ThreadDumpAction } + modifiedActions.add(pos + 1, ActionManager.getInstance().getAction("Kotlin.XDebugger.CoroutinesDump")) + ui.options.setLeftToolbar(DefaultActionGroup(modifiedActions), ActionPlaces.DEBUGGER_TOOLBAR) + return true + } +} + +val Project.coroutineConnectionListener by projectListener + +val projectListener + get() = object : ReadOnlyProperty { + lateinit var listenerProject: CoroutineProjectConnectionListener + + override fun getValue(project: Project, property: KProperty<*>): CoroutineProjectConnectionListener { + if (!::listenerProject.isInitialized) + listenerProject = CoroutineProjectConnectionListener(project) + return listenerProject + } + } + +internal fun coroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines") + +internal fun initializeCoroutineAgent(params: JavaParameters, it: String?) { + params.vmParametersList?.add("-javaagent:$it") + params.vmParametersList?.add("-ea") +} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineXDebuggerManagerListener.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineXDebuggerManagerListener.kt deleted file mode 100644 index 180f36a572e..00000000000 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineXDebuggerManagerListener.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -package org.jetbrains.kotlin.idea.debugger.coroutines - -import com.intellij.debugger.DebuggerInvocationUtil -import com.intellij.debugger.DebuggerManagerEx -import com.intellij.debugger.actions.ThreadDumpAction -import com.intellij.debugger.impl.DebuggerSession -import com.intellij.execution.configurations.JavaParameters -import com.intellij.execution.configurations.RunConfigurationBase -import com.intellij.execution.ui.RunnerLayoutUi -import com.intellij.execution.ui.layout.PlaceInGrid -import com.intellij.execution.ui.layout.impl.RunnerContentUi -import com.intellij.execution.ui.layout.impl.RunnerLayoutUiImpl -import com.intellij.icons.AllIcons -import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.actionSystem.ActionPlaces -import com.intellij.openapi.actionSystem.DefaultActionGroup -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Key -import com.intellij.openapi.util.registry.Registry -import com.intellij.ui.content.ContentManagerAdapter -import com.intellij.ui.content.ContentManagerEvent -import com.intellij.util.messages.MessageBusConnection -import com.intellij.xdebugger.XDebugProcess -import com.intellij.xdebugger.XDebuggerManager -import com.intellij.xdebugger.XDebuggerManagerListener -import org.jetbrains.kotlin.psi.UserDataProperty - -class CoroutineXDebuggerManagerListener(val project: Project) : XDebuggerManagerListener { - lateinit var connection: MessageBusConnection - - override fun processStarted(debugProcess: XDebugProcess) { - DebuggerInvocationUtil.swingInvokeLater(project) { - val session = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession - val ui = session?.xDebugSession?.ui - if (ui != null) - registerCoroutinesPanel(ui, session) - } - } - - override fun processStopped(debugProcess: XDebugProcess) { - connection.disconnect() - project.listenerCreated = false - } - - /** - * Adds panel to XDebugSessionTab - */ - private fun registerCoroutinesPanel(ui: RunnerLayoutUi, session: DebuggerSession) { - val panel = CoroutinesPanel(session.project, session.contextManager) - val content = ui.createContent( - "CoroutinesContent", panel, "Coroutines", // TODO(design) - AllIcons.Debugger.ThreadGroup, null - ) - content.isCloseable = false - ui.addContent(content, 0, PlaceInGrid.left, true) - ui.addListener(object : ContentManagerAdapter() { - override fun selectionChanged(event: ContentManagerEvent) { - if (event.content === content) { - if (content.isSelected) { - panel.setUpdateEnabled(true) - if (panel.isRefreshNeeded) { - panel.rebuildIfVisible(DebuggerSession.Event.CONTEXT) - } - } else { - panel.setUpdateEnabled(false) - } - } - } - }, content) - // add coroutine dump button: due to api problem left toolbar is copied, modified and reset to tab - val runnerContent = (ui.options as RunnerLayoutUiImpl).getData(RunnerContentUi.KEY.name) as RunnerContentUi - val modifiedActions = runnerContent.getActions(true) - val pos = modifiedActions.indexOfLast { it is ThreadDumpAction } - modifiedActions.add(pos + 1, ActionManager.getInstance().getAction("Kotlin.XDebugger.CoroutinesDump")) - ui.options.setLeftToolbar(DefaultActionGroup(modifiedActions), ActionPlaces.DEBUGGER_TOOLBAR) - } - - fun attach() { - connection = project.messageBus.connect() - connection.subscribe(XDebuggerManager.TOPIC, this) - project.listenerCreated = true - } -} - -internal var Project.listenerCreated: Boolean? by UserDataProperty(Key.create("COROUTINES_DEBUG_TAB_CREATE_LISTENER")) - -internal fun isCoroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines") - -internal fun initializeCoroutineAgent(params: JavaParameters, it: String?) { - params.vmParametersList?.add("-javaagent:$it") - params.vmParametersList?.add("-ea") -} - -internal fun ?> registerProjectCoroutineListener(configuration: T) { - val project = (configuration as RunConfigurationBase<*>).project - // add listener to put coroutines tab into debugger tab - if (project.listenerCreated != true) { // prevent multiple listeners creation - CoroutineXDebuggerManagerListener(project).attach() - } -} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt index f8ef97fcf66..9be67bb6bec 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt @@ -9,31 +9,22 @@ import com.intellij.execution.configurations.DebuggingRunnerData import com.intellij.execution.configurations.JavaParameters import com.intellij.execution.configurations.RunConfigurationBase import com.intellij.execution.configurations.RunnerSettings -import com.intellij.openapi.diagnostic.Logger /** - * Installs coroutines debug agent and coroutines tab if `kotlinx.coroutines.debug` dependency is found + * Installs coroutines debug agent and coroutines tab if `kotlinx-coroutines-debug` dependency is found */ @Suppress("IncompatibleAPI") class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() { - private val log = Logger.getInstance(this::class.java) - override fun isApplicableFor(configuration: RunConfigurationBase<*>) = isCoroutineDebuggerEnabled() + override fun isApplicableFor(configuration: RunConfigurationBase<*>) = coroutineDebuggerEnabled() override fun ?> updateJavaParameters( configuration: T, params: JavaParameters?, runnerSettings: RunnerSettings? ) { - if (!isCoroutineDebuggerEnabled()) return - if (runnerSettings is DebuggingRunnerData) { - try { - val kotlinxCoroutinesClassPathLib = params?.classPath?.pathList?.first { it.contains("kotlinx-coroutines-debug") } - initializeCoroutineAgent(params!!, kotlinxCoroutinesClassPathLib) - registerProjectCoroutineListener(configuration) - } catch (e: NoSuchElementException) { - log.warn("'kotlinx-coroutines-debug' not found in classpath. Coroutine debugger disabled.") - } + if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) { + configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings) } } -} \ No newline at end of file +} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt.193 b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt.193 index 935e04f49fe..9d54e256c42 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt.193 +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt.193 @@ -9,31 +9,22 @@ import com.intellij.execution.configurations.DebuggingRunnerData import com.intellij.execution.configurations.JavaParameters import com.intellij.execution.configurations.RunConfigurationBase import com.intellij.execution.configurations.RunnerSettings -import com.intellij.openapi.diagnostic.Logger /** - * Installs coroutines debug agent and coroutines tab if `kotlinx.coroutines.debug` dependency is found + * Installs coroutines debug agent and coroutines tab if `kotlinx-coroutines-debug` dependency is found */ @Suppress("IncompatibleAPI") class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() { - private val log = Logger.getInstance(this::class.java) - override fun isApplicableFor(configuration: RunConfigurationBase<*>) = isCoroutineDebuggerEnabled() + override fun isApplicableFor(configuration: RunConfigurationBase<*>) = coroutineDebuggerEnabled() override fun ?> updateJavaParameters( configuration: T, params: JavaParameters, runnerSettings: RunnerSettings? ) { - if (!isCoroutineDebuggerEnabled()) return - if (runnerSettings is DebuggingRunnerData) { - try { - val kotlinxCoroutinesClassPathLib = params?.classPath?.pathList?.first { it.contains("kotlinx-coroutines-debug") } - initializeCoroutineAgent(params!!, kotlinxCoroutinesClassPathLib) - registerProjectCoroutineListener(configuration) - } catch (e: NoSuchElementException) { - log.warn("'kotlinx-coroutines-debug' not found in classpath. Coroutine debugger disabled.") - } + if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) { + configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings) } } -} \ No newline at end of file +} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugProbesProxy.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugProbesProxy.kt deleted file mode 100644 index 58c768afd33..00000000000 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugProbesProxy.kt +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ -package org.jetbrains.kotlin.idea.debugger.coroutines - -import com.intellij.debugger.engine.DebugProcess -import com.intellij.openapi.util.Key -import com.sun.jdi.* -import javaslang.control.Either -import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext -import org.jetbrains.kotlin.psi.UserDataProperty - -object CoroutinesDebugProbesProxy { - private const val DEBUG_PACKAGE = "kotlinx.coroutines.debug" - private var DebugProcess.references by UserDataProperty(Key.create("COROUTINES_DEBUG_REFERENCES")) - - @Synchronized - @Suppress("unused") - fun install(context: ExecutionContext) { - val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType - val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference } - val install = debugProbes.concreteMethodByName("install", "()V") - context.invokeMethod(instance, install, emptyList()) - } - - @Synchronized - @Suppress("unused") - fun uninstall(context: ExecutionContext) { - val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType - val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference } - val uninstall = debugProbes.concreteMethodByName("uninstall", "()V") - context.invokeMethod(instance, uninstall, emptyList()) - } - - /** - * Invokes DebugProbes from debugged process's classpath and returns states of coroutines - * Should be invoked on debugger manager thread - */ - @Synchronized - fun dumpCoroutines(context: ExecutionContext): Either> { - try { - var refs = context.debugProcess.references - if (refs == null) { - refs = ProcessReferences(context) - context.debugProcess.references = refs - } - // get dump - val infoList = context.invokeMethod(refs.instance, refs.dumpMethod, emptyList()) as? ObjectReference - ?: return Either.right(emptyList()) - - context.keepReference(infoList) - val size = (context.invokeMethod(infoList, refs.getSize, emptyList()) as IntegerValue).value() - - return Either.right(List(size) { - val index = context.vm.mirrorOf(it) - // `List.get(index)` - val elem = context.invokeMethod(infoList, refs.getElement, listOf(index)) as ObjectReference - val name = getName(context, elem, refs) - val state = getState(context, elem, refs) - val thread = getLastObservedThread(elem, refs.threadRef) - CoroutineState( - name, - CoroutineState.State.valueOf(state), - thread, - getStackTrace(elem, refs, context), - elem.getValue(refs.continuation) as? ObjectReference - ) - }) - } catch (e: Throwable) { - return Either.left(e) - } - } - - private fun getName( - context: ExecutionContext, // Execution context to invoke methods - info: ObjectReference, // CoroutineInfo instance - refs: ProcessReferences - ): String { - // equals to `coroutineInfo.context.get(CoroutineName).name` - val coroutineContextInst = context.invokeMethod( - info, - refs.getContext, - emptyList() - ) as? ObjectReference ?: throw IllegalArgumentException("Coroutine context must not be null") - val coroutineName = context.invokeMethod( - coroutineContextInst, - refs.getContextElement, listOf(refs.nameKey) - ) as? ObjectReference - // If the coroutine doesn't have a given name, CoroutineContext.get(CoroutineName) returns null - val name = if (coroutineName != null) (context.invokeMethod( - coroutineName, - refs.getName, - emptyList() - ) as StringReference).value() else "coroutine" - val id = (info.getValue(refs.idField) as LongValue).value() - return "$name#$id" - } - - private fun getState( - context: ExecutionContext, // Execution context to invoke methods - info: ObjectReference, // CoroutineInfo instance - refs: ProcessReferences - ): String { - // equals to `stringState = coroutineInfo.state.toString()` - val state = context.invokeMethod(info, refs.getState, emptyList()) as ObjectReference - return (context.invokeMethod(state, refs.toString, emptyList()) as StringReference).value() - } - - private fun getLastObservedThread( - info: ObjectReference, // CoroutineInfo instance - threadRef: Field // reference to lastObservedThread - ): ThreadReference? = info.getValue(threadRef) as? ThreadReference - - /** - * Returns list of stackTraceElements for the given CoroutineInfo's [ObjectReference] - */ - private fun getStackTrace( - info: ObjectReference, - refs: ProcessReferences, - context: ExecutionContext - ): List { - val frameList = context.invokeMethod(info, refs.lastObservedStackTrace, emptyList()) as ObjectReference - val mergedFrameList = context.invokeMethod( - refs.debugProbesImpl, - refs.enhanceStackTraceWithThreadDump, listOf(info, frameList) - ) as ObjectReference - val size = (context.invokeMethod(mergedFrameList, refs.getSize, emptyList()) as IntegerValue).value() - - val list = ArrayList() - for (it in size - 1 downTo 0) { - val frame = context.invokeMethod( - mergedFrameList, refs.getElement, - listOf(context.vm.virtualMachine.mirrorOf(it)) - ) as ObjectReference - val clazz = (frame.getValue(refs.className) as? StringReference)?.value() - list.add( - 0, // add in the beginning - StackTraceElement( - clazz, - (frame.getValue(refs.methodName) as? StringReference)?.value(), - (frame.getValue(refs.fileName) as? StringReference)?.value(), - (frame.getValue(refs.line) as IntegerValue).value() - ) - ) - } - return list - } - - /** - * Holds ClassTypes, Methods, ObjectReferences and Fields for a particular jvm - */ - private class ProcessReferences(context: ExecutionContext) { - // kotlinx.coroutines.debug.DebugProbes instance and methods - val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType - val probesImplType = context.findClass("$DEBUG_PACKAGE.internal.DebugProbesImpl") as ClassType - val debugProbesImpl = with(probesImplType) { getValue(fieldByName("INSTANCE")) as ObjectReference } - val enhanceStackTraceWithThreadDump: Method = probesImplType - .methodsByName("enhanceStackTraceWithThreadDump").single() - - val dumpMethod: Method = debugProbes.concreteMethodByName("dumpCoroutinesInfo", "()Ljava/util/List;") - val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference } - - // CoroutineInfo - val info = context.findClass("$DEBUG_PACKAGE.CoroutineInfo") as ClassType - val getState: Method = info.concreteMethodByName("getState", "()Lkotlinx/coroutines/debug/State;") - val getContext: Method = info.concreteMethodByName("getContext", "()Lkotlin/coroutines/CoroutineContext;") - val idField: Field = info.fieldByName("sequenceNumber") - val lastObservedStackTrace: Method = info.methodsByName("lastObservedStackTrace").single() - val coroutineContext = context.findClass("kotlin.coroutines.CoroutineContext") as InterfaceType - val getContextElement: Method = coroutineContext.methodsByName("get").single() - val coroutineName = context.findClass("kotlinx.coroutines.CoroutineName") as ClassType - val getName: Method = coroutineName.methodsByName("getName").single() - val nameKey = coroutineName.getValue(coroutineName.fieldByName("Key")) as ObjectReference - val toString: Method = (context.findClass("java.lang.Object") as ClassType) - .concreteMethodByName("toString", "()Ljava/lang/String;") - - val threadRef: Field = info.fieldByName("lastObservedThread") - val continuation: Field = info.fieldByName("lastObservedFrame") - - // Methods for list - val listType = context.findClass("java.util.List") as InterfaceType - val getSize: Method = listType.methodsByName("size").single() - val getElement: Method = listType.methodsByName("get").single() - val element = context.findClass("java.lang.StackTraceElement") as ClassType - - // for StackTraceElement - val methodName: Field = element.fieldByName("methodName") - val className: Field = element.fieldByName("declaringClass") - val fileName: Field = element.fieldByName("fileName") - val line: Field = element.fieldByName("lineNumber") - } -} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebuggerTree.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebuggerTree.kt deleted file mode 100644 index a0c2dac5d5b..00000000000 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebuggerTree.kt +++ /dev/null @@ -1,451 +0,0 @@ -/* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -package org.jetbrains.kotlin.idea.debugger.coroutines - -import com.intellij.debugger.DebuggerBundle -import com.intellij.debugger.DebuggerInvocationUtil -import com.intellij.debugger.DebuggerManagerEx -import com.intellij.debugger.actions.GotoFrameSourceAction -import com.intellij.debugger.engine.* -import com.intellij.debugger.engine.evaluation.EvaluateException -import com.intellij.debugger.engine.evaluation.EvaluationContextImpl -import com.intellij.debugger.engine.events.DebuggerCommandImpl -import com.intellij.debugger.engine.events.SuspendContextCommandImpl -import com.intellij.debugger.impl.DebuggerContextImpl -import com.intellij.debugger.impl.DebuggerSession -import com.intellij.debugger.jdi.StackFrameProxyImpl -import com.intellij.debugger.jdi.ThreadReferenceProxyImpl -import com.intellij.debugger.memory.utils.StackFrameItem -import com.intellij.debugger.ui.impl.tree.TreeBuilder -import com.intellij.debugger.ui.impl.tree.TreeBuilderNode -import com.intellij.debugger.ui.impl.watch.* -import com.intellij.debugger.ui.tree.StackFrameDescriptor -import com.intellij.ide.DataManager -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.MessageType -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.search.GlobalSearchScope -import com.intellij.ui.DoubleClickListener -import com.intellij.xdebugger.XDebuggerUtil -import com.intellij.xdebugger.XSourcePosition -import com.intellij.xdebugger.frame.XExecutionStack -import com.intellij.xdebugger.impl.XDebuggerManagerImpl -import com.intellij.xdebugger.impl.ui.DebuggerUIUtil -import com.sun.jdi.ClassType -import javaslang.control.Either -import org.jetbrains.kotlin.idea.debugger.AsyncStackTraceContext -import org.jetbrains.kotlin.idea.debugger.KotlinCoroutinesAsyncStackTraceProvider -import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext -import java.awt.event.MouseEvent -import java.lang.ref.WeakReference -import javax.swing.event.TreeModelEvent -import javax.swing.event.TreeModelListener - -/** - * Tree of coroutines for [CoroutinesPanel] - */ -class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) { - private val logger = Logger.getInstance(this::class.java) - private var lastSuspendContextCache: Cache? = null - - override fun createNodeManager(project: Project): NodeManagerImpl { - return object : NodeManagerImpl(project, this) { - override fun getContextKey(frame: StackFrameProxyImpl?): String? { - return "CoroutinesView" - } - } - } - - /** - * Prepare specific behavior instead of DebuggerTree constructor - */ - init { - val model = object : TreeBuilder(this) { - override fun buildChildren(node: TreeBuilderNode) { - val debuggerTreeNode = node as DebuggerTreeNodeImpl - if (debuggerTreeNode.descriptor is DefaultNodeDescriptor) { - return - } - - node.add(myNodeManager.createMessageNode(MessageDescriptor.EVALUATING)) - buildNode(debuggerTreeNode) - } - - override fun isExpandable(builderNode: TreeBuilderNode): Boolean { - return this@CoroutinesDebuggerTree.isExpandable(builderNode as DebuggerTreeNodeImpl) - } - } - model.setRoot(nodeFactory.defaultNode) - model.addTreeModelListener(createListener()) - - setModel(model) - emptyText.text = "Coroutines are not available" - } - - /** - * Add frames inside coroutine (node) - */ - private fun buildNode(node: DebuggerTreeNodeImpl) { - val context = DebuggerManagerEx.getInstanceEx(project).context - val debugProcess = context.debugProcess - debugProcess?.managerThread?.schedule(object : SuspendContextCommandImpl(context.suspendContext) { - override fun contextAction(suspendContext: SuspendContextImpl) { - val evalContext = debuggerContext.createEvaluationContext() ?: return - if (node.descriptor is CoroutineDescriptorImpl || node.descriptor is CreationFramesDescriptor) { - val children = mutableListOf() - try { - addChildren(children, debugProcess, node.descriptor, evalContext) - } catch (e: EvaluateException) { - children.clear() - children.add(myNodeManager.createMessageNode(e.message)) - logger.debug(e) - } - DebuggerInvocationUtil.swingInvokeLater(project) { - node.removeAllChildren() - for (debuggerTreeNode in children) { - node.add(debuggerTreeNode) - } - node.childrenChanged(true) - } - } - } - }) - } - - fun installAction(): () -> Unit { - val listener = object : DoubleClickListener() { - override fun onDoubleClick(e: MouseEvent): Boolean { - val location = getPathForLocation(e.x, e.y) - ?.lastPathComponent as? DebuggerTreeNodeImpl ?: return false - return selectFrame(location.userObject) - } - } - listener.installOn(this) - - return { listener.uninstall(this) } - } - - fun selectFrame(descriptor: Any): Boolean { - val dataContext = DataManager.getInstance().getDataContext(this@CoroutinesDebuggerTree) - val context = DebuggerManagerEx.getInstanceEx(project).context - when (descriptor) { - is SuspendStackFrameDescriptor -> { - buildSuspendStackFrameChildren(descriptor) - return true - } - is AsyncStackFrameDescriptor -> { - buildAsyncStackFrameChildren(descriptor, context.debugProcess ?: return false) - return true - } - is EmptyStackFrameDescriptor -> { - buildEmptyStackFrameChildren(descriptor) - return true - } - is StackFrameDescriptor -> { - GotoFrameSourceAction.doAction(dataContext) - return true - } - else -> return true - } - } - - private fun buildSuspendStackFrameChildren(descriptor: SuspendStackFrameDescriptor) { - val context = DebuggerManagerEx.getInstanceEx(project).context - val pos = getPosition(descriptor.frame) ?: return - context.debugProcess?.managerThread?.schedule(object : SuspendContextCommandImpl(context.suspendContext) { - override fun contextAction() { - val (stack, stackFrame) = createSyntheticStackFrame(descriptor, pos) ?: return - val action: () -> Unit = { context.debuggerSession?.xDebugSession?.setCurrentStackFrame(stack, stackFrame) } - ApplicationManager.getApplication() - .invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree)) - } - }) - } - - private fun buildAsyncStackFrameChildren(descriptor: AsyncStackFrameDescriptor, process: DebugProcessImpl) { - process.managerThread?.schedule(object : DebuggerCommandImpl() { - override fun action() { - val context = DebuggerManagerEx.getInstanceEx(project).context - val proxy = ThreadReferenceProxyImpl( - process.virtualMachineProxy, - descriptor.state.thread // is not null because it's a running coroutine - ) - val executionStack = JavaExecutionStack(proxy, process, false) - executionStack.initTopFrame() - val frame = descriptor.frame.createFrame(process) - DebuggerUIUtil.invokeLater { - context.debuggerSession?.xDebugSession?.setCurrentStackFrame( - executionStack, - frame - ) - } - } - }) - } - - private fun buildEmptyStackFrameChildren(descriptor: EmptyStackFrameDescriptor) { - val position = getPosition(descriptor.frame) ?: return - val context = DebuggerManagerEx.getInstanceEx(project).context - val suspendContext = context.suspendContext ?: return - val proxy = suspendContext.thread ?: return - context.debugProcess?.managerThread?.schedule(object : DebuggerCommandImpl() { - override fun action() { - val executionStack = - JavaExecutionStack(proxy, context.debugProcess!!, false) - executionStack.initTopFrame() - val frame = SyntheticStackFrame(descriptor, emptyList(), position) - val action: () -> Unit = - { context.debuggerSession?.xDebugSession?.setCurrentStackFrame(executionStack, frame) } - ApplicationManager.getApplication() - .invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree)) - } - }) - } - - private fun getPosition(frame: StackTraceElement): XSourcePosition? { - - val psiFacade = JavaPsiFacade.getInstance(project) - val psiClass = psiFacade.findClass( - frame.className.substringBefore("$"), // find outer class, for which psi exists TODO - GlobalSearchScope.everythingScope(project) - ) - val classFile = psiClass?.containingFile?.virtualFile - // to convert to 0-based line number or '-1' to do not move - val lineNumber = if(frame.lineNumber > 0) frame.lineNumber - 1 else return null - return XDebuggerUtil.getInstance().createPosition(classFile, lineNumber) - } - - /** - * Should be invoked on manager thread - */ - private fun createSyntheticStackFrame( - descriptor: SuspendStackFrameDescriptor, - pos: XSourcePosition - ): Pair? { - val context = DebuggerManagerEx.getInstanceEx(project).context - val suspendContext = context.suspendContext ?: return null - val proxy = suspendContext.thread ?: return null - val executionStack = JavaExecutionStack(proxy, suspendContext.debugProcess, false) - executionStack.initTopFrame() - val evalContext = context.createEvaluationContext() - val frameProxy = evalContext?.frameProxy ?: return null - val execContext = ExecutionContext(evalContext, frameProxy) - val continuation = descriptor.continuation // guaranteed that it is a BaseContinuationImpl - val aMethod = (continuation.type() as ClassType).concreteMethodByName( - "getStackTraceElement", - "()Ljava/lang/StackTraceElement;" - ) - val debugMetadataKtType = execContext - .findClass("kotlin.coroutines.jvm.internal.DebugMetadataKt") as ClassType - val vars = with(KotlinCoroutinesAsyncStackTraceProvider()) { - AsyncStackTraceContext( - execContext, - aMethod, - debugMetadataKtType - ).getSpilledVariables(continuation) - } ?: return null - return executionStack to SyntheticStackFrame(descriptor, vars, pos) - } - - private fun addChildren( - children: MutableList, - debugProcess: DebugProcessImpl, - descriptor: NodeDescriptorImpl, - evalContext: EvaluationContextImpl - ) { - val creationStackTraceSeparator = "\b\b\b" // the "\b\b\b" is used for creation stacktrace separator in kotlinx.coroutines - if (descriptor !is CoroutineDescriptorImpl) { - if (descriptor is CreationFramesDescriptor) { - val threadProxy = debuggerContext.suspendContext?.thread ?: return - val proxy = threadProxy.forceFrames().first() - descriptor.frames.forEach { - children.add(myNodeManager.createNode(EmptyStackFrameDescriptor(it, proxy), evalContext)) - } - } - return - } - when (descriptor.state.state) { - CoroutineState.State.RUNNING -> { - if (descriptor.state.thread == null) { - children.add(myNodeManager.createMessageNode("Frames are not available")) - return - } - val proxy = ThreadReferenceProxyImpl( - debugProcess.virtualMachineProxy, - descriptor.state.thread - ) - val frames = proxy.forceFrames() - var i = frames.lastIndex - while (i > 0 && frames[i].location().method().name() != "resumeWith") i-- - // if i is less than 0, wait, what? - for (frame in 0..--i) { - children.add(createFrameDescriptor(descriptor, evalContext, frames[frame])) - } - if (i > 0) { // add async stack trace if there are frames after invokeSuspend - val async = KotlinCoroutinesAsyncStackTraceProvider().getAsyncStackTrace( - JavaStackFrame(StackFrameDescriptorImpl(frames[i - 1], MethodsTracker()), true), - evalContext.suspendContext - ) - async?.forEach { children.add(createAsyncFrameDescriptor(descriptor, evalContext, it, frames[0])) } - } - for (frame in i + 2..frames.lastIndex) { - children.add(createFrameDescriptor(descriptor, evalContext, frames[frame])) - } - } - CoroutineState.State.SUSPENDED -> { - val threadProxy = debuggerContext.suspendContext?.thread ?: return - val proxy = threadProxy.forceFrames().first() - // the thread is paused on breakpoint - it has at least one frame - for (it in descriptor.state.stackTrace) { - if (it.className.startsWith(creationStackTraceSeparator)) break - children.add(createCoroutineFrameDescriptor(descriptor, evalContext, it, proxy)) - } - } - else -> { - } - } - val trace = descriptor.state.stackTrace - val index = trace.indexOfFirst { it.className.startsWith(creationStackTraceSeparator) } - children.add(myNodeManager.createNode(CreationFramesDescriptor(trace.subList(index + 1, trace.size)), evalContext)) - } - - - private fun createFrameDescriptor( - descriptor: NodeDescriptorImpl, - evalContext: EvaluationContextImpl, - frame: StackFrameProxyImpl - ): DebuggerTreeNodeImpl { - return myNodeManager.createNode( - myNodeManager.getStackFrameDescriptor(descriptor, frame), - evalContext - ) - } - - private fun createCoroutineFrameDescriptor( - descriptor: CoroutineDescriptorImpl, - evalContext: EvaluationContextImpl, - frame: StackTraceElement, - proxy: StackFrameProxyImpl, - parent: NodeDescriptorImpl? = null - ): DebuggerTreeNodeImpl { - return myNodeManager.createNode( - myNodeManager.getDescriptor( - parent, - CoroutineStackFrameData(descriptor.state, frame, proxy) - ), evalContext - ) - } - - private fun createAsyncFrameDescriptor( - descriptor: CoroutineDescriptorImpl, - evalContext: EvaluationContextImpl, - frame: StackFrameItem, - proxy: StackFrameProxyImpl - ): DebuggerTreeNodeImpl { - return myNodeManager.createNode( - myNodeManager.getDescriptor( - descriptor, - CoroutineStackFrameData(descriptor.state, frame, proxy) - ), evalContext - ) - } - - private fun createListener() = object : TreeModelListener { - override fun treeNodesChanged(event: TreeModelEvent) { - hideTooltip() - } - - override fun treeNodesInserted(event: TreeModelEvent) { - hideTooltip() - } - - override fun treeNodesRemoved(event: TreeModelEvent) { - hideTooltip() - } - - override fun treeStructureChanged(event: TreeModelEvent) { - hideTooltip() - } - } - - override fun isExpandable(node: DebuggerTreeNodeImpl): Boolean { - val descriptor = node.descriptor - return if (descriptor is StackFrameDescriptor) false else descriptor.isExpandable - } - - override fun build(context: DebuggerContextImpl) { - val session = context.debuggerSession - val command = RefreshCoroutinesTreeCommand(session, context.suspendContext) - - val state = if (session != null) session.state else DebuggerSession.State.DISPOSED - if (ApplicationManager.getApplication().isUnitTestMode - || state == DebuggerSession.State.PAUSED - ) { - showMessage(MessageDescriptor.EVALUATING) - context.debugProcess!!.managerThread.schedule(command) - } else { - showMessage(if (session != null) session.stateDescription else DebuggerBundle.message("status.debug.stopped")) - } - } - - private inner class RefreshCoroutinesTreeCommand(private val mySession: DebuggerSession?, context: SuspendContextImpl?) : - SuspendContextCommandImpl(context) { - - override fun contextAction() { - val root = nodeFactory.defaultNode - mySession ?: return - val suspendContext = suspendContext - if (suspendContext == null || suspendContext.isResumed) { - setRoot(root.apply { add(myNodeManager.createMessageNode("Application is resumed")) }) - return - } - val evaluationContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy) - val executionContext = ExecutionContext(evaluationContext, suspendContext.frameProxy ?: return) - val cache = lastSuspendContextCache - val states = if (cache != null && cache.first.get() === suspendContext) { - cache.second - } else CoroutinesDebugProbesProxy.dumpCoroutines(executionContext).apply { - lastSuspendContextCache = WeakReference(suspendContext) to this - } - // if suspend context hasn't changed - use last dump, else compute new - if (states.isLeft) { - logger.warn(states.left) - setRoot(root.apply { - clear() - add(nodeFactory.createMessageNode(MessageDescriptor("Dump failed"))) - }) - XDebuggerManagerImpl.NOTIFICATION_GROUP - .createNotification( - "Coroutine dump failed. See log", - MessageType.ERROR - ).notify(project) - return - } - for (state in states.get()) { - root.add( - nodeFactory.createNode( - nodeFactory.getDescriptor(null, CoroutineData(state)), evaluationContext - ) - ) - } - setRoot(root) - } - - private fun setRoot(root: DebuggerTreeNodeImpl) { - DebuggerInvocationUtil.swingInvokeLater(project) { - mutableModel.setRoot(root) - treeChanged() - } - } - - } - -} - -private typealias Cache = Pair, Either>> diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesPanel.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesPanel.kt deleted file mode 100644 index bbc8c4e83ba..00000000000 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesPanel.kt +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -@file:Suppress("DEPRECATION") - -package org.jetbrains.kotlin.idea.debugger.coroutines - -import com.intellij.debugger.engine.events.SuspendContextCommandImpl -import com.intellij.debugger.impl.DebuggerContextImpl -import com.intellij.debugger.impl.DebuggerContextListener -import com.intellij.debugger.impl.DebuggerSession -import com.intellij.debugger.impl.DebuggerStateManager -import com.intellij.debugger.ui.impl.DebuggerTreePanel -import com.intellij.debugger.ui.impl.watch.DebuggerTree -import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl -import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.actionSystem.ActionPopupMenu -import com.intellij.openapi.actionSystem.EmptyActionGroup -import com.intellij.openapi.actionSystem.PlatformDataKeys -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer -import com.intellij.ui.ScrollPaneFactory -import com.intellij.util.Alarm -import org.jetbrains.annotations.NonNls -import java.awt.BorderLayout -import java.awt.event.KeyAdapter -import java.awt.event.KeyEvent -import java.util.* - -/** - * Actually added into ui in [CoroutineXDebuggerManagerListener.registerCoroutinesPanel] - * Some methods are copied from [com.intellij.debugger.ui.impl.ThreadsPanel] - */ -class CoroutinesPanel(project: Project, stateManager: DebuggerStateManager) : DebuggerTreePanel(project, stateManager) { - private val myUpdateLabelsAlarm = Alarm(Alarm.ThreadToUse.SWING_THREAD) - - init { - val disposable = getCoroutinesTree().installAction() - registerDisposable(disposable) - getCoroutinesTree().addKeyListener(object : KeyAdapter() { - override fun keyPressed(e: KeyEvent) { - if (e.keyCode == KeyEvent.VK_ENTER && getCoroutinesTree().selectionCount == 1) { - val selected = getCoroutinesTree().selectionModel.selectionPath.lastPathComponent - if (selected is DebuggerTreeNodeImpl) getCoroutinesTree().selectFrame(selected.userObject) - } - } - }) - add(ScrollPaneFactory.createScrollPane(getCoroutinesTree()), BorderLayout.CENTER) - stateManager.addListener(object : DebuggerContextListener { - override fun changeEvent(newContext: DebuggerContextImpl, event: DebuggerSession.Event) { - if (DebuggerSession.Event.ATTACHED == event || DebuggerSession.Event.RESUME == event) { - startLabelsUpdate() - } else if (DebuggerSession.Event.PAUSE == event - || DebuggerSession.Event.DETACHED == event - || DebuggerSession.Event.DISPOSE == event - ) { - myUpdateLabelsAlarm.cancelAllRequests() - } - if (DebuggerSession.Event.DETACHED == event || DebuggerSession.Event.DISPOSE == event) { - stateManager.removeListener(this) - } - } - }) - startLabelsUpdate() - } - - private fun startLabelsUpdate() { - if (myUpdateLabelsAlarm.isDisposed) return - myUpdateLabelsAlarm.cancelAllRequests() - myUpdateLabelsAlarm.addRequest(object : Runnable { - override fun run() { - var updateScheduled = false - try { - if (isUpdateEnabled) { - val tree = getCoroutinesTree() - val root = tree.model.root as DebuggerTreeNodeImpl - val process = context.debugProcess - if (process != null) { - process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) { - override fun contextAction() { - try { - updateNodeLabels(root) - } finally { - reschedule() - } - } - - override fun commandCancelled() { - reschedule() - } - }) - updateScheduled = true - } - } - } finally { - if (!updateScheduled) { - reschedule() - } - } - } - - private fun reschedule() { - val session = context.debuggerSession - if (session != null && session.isAttached && !session.isPaused && !myUpdateLabelsAlarm.isDisposed) { - myUpdateLabelsAlarm.addRequest( - this, - LABELS_UPDATE_DELAY_MS, ModalityState.NON_MODAL - ) - } - } - - }, LABELS_UPDATE_DELAY_MS, ModalityState.NON_MODAL) - } - - override fun dispose() { - Disposer.dispose(myUpdateLabelsAlarm) - super.dispose() - } - - // copied from com.intellij.debugger.ui.impl.ThreadsPanel.updateNodeLabels - private fun updateNodeLabels(from: DebuggerTreeNodeImpl) { - val children = from.children() - try { - while (children.hasMoreElements()) { - val child = children.nextElement() as DebuggerTreeNodeImpl - child.descriptor.updateRepresentation( - null - ) { child.labelChanged() } - updateNodeLabels(child) - } - } catch (ignored: NoSuchElementException) { // children have changed - just skip - } - - } - - override fun createTreeView(): DebuggerTree { - return CoroutinesDebuggerTree(project) - } - - override fun createPopupMenu(): ActionPopupMenu { - val group = EmptyActionGroup() - return ActionManager.getInstance().createActionPopupMenu("Debugger.CoroutinesPanelPopup", group) - } - - override fun getData(dataId: String): Any? { - return if (PlatformDataKeys.HELP_ID.`is`(dataId)) { - HELP_ID - } else super.getData(dataId) - } - - fun getCoroutinesTree(): CoroutinesDebuggerTree = tree as CoroutinesDebuggerTree - - companion object { - @NonNls - private val HELP_ID = "debugging.debugCoroutines" - private const val LABELS_UPDATE_DELAY_MS = 200 - } - -} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/BuildCoroutineNodeCommand.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/BuildCoroutineNodeCommand.kt new file mode 100644 index 00000000000..9e4ad372e51 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/BuildCoroutineNodeCommand.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.command + +import com.intellij.debugger.DebuggerInvocationUtil +import com.intellij.debugger.engine.events.DebuggerContextCommandImpl +import com.intellij.debugger.impl.DebuggerContextImpl +import com.intellij.debugger.impl.PrioritizedTask +import com.intellij.debugger.jdi.ThreadReferenceProxyImpl +import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl +import com.intellij.debugger.ui.impl.watch.NodeManagerImpl + +open class BuildCoroutineNodeCommand( + val node: DebuggerTreeNodeImpl, + debuggerContext: DebuggerContextImpl, + val myNodeManager: NodeManagerImpl, + thread: ThreadReferenceProxyImpl? = null +) : DebuggerContextCommandImpl(debuggerContext, thread) { + + protected val myChildren = mutableListOf() + + override fun getPriority() = PrioritizedTask.Priority.NORMAL + + protected fun updateUI(scrollToVisible: Boolean) { + DebuggerInvocationUtil.swingInvokeLater(debuggerContext.project) { + node.removeAllChildren() + for (debuggerTreeNode in myChildren) { + node.add(debuggerTreeNode) + } + node.childrenChanged(scrollToVisible) + } + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/CoroutineBuildCreationFrameCommand.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/CoroutineBuildCreationFrameCommand.kt new file mode 100644 index 00000000000..bf805774d83 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/CoroutineBuildCreationFrameCommand.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.command + +import com.intellij.debugger.impl.DebuggerContextImpl +import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl +import com.intellij.debugger.ui.impl.watch.NodeManagerImpl +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CreationFramesDescriptor +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineCreatedStackFrameDescriptor + +class CoroutineBuildCreationFrameCommand( + node: DebuggerTreeNodeImpl, + val descriptor: CreationFramesDescriptor, + nodeManager: NodeManagerImpl, + debuggerContext: DebuggerContextImpl +) : BuildCoroutineNodeCommand(node, debuggerContext, nodeManager) { + override fun threadAction() { + val threadProxy = debuggerContext.suspendContext?.thread ?: return + val evalContext = debuggerContext.createEvaluationContext() ?: return + val proxy = threadProxy.forceFrames().first() + descriptor.frames.forEach { + val descriptor = myNodeManager.createNode( + CoroutineCreatedStackFrameDescriptor(it, proxy), evalContext) + myChildren.add(descriptor) + } + updateUI(true) + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/CoroutineBuildFrameCommand.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/CoroutineBuildFrameCommand.kt new file mode 100644 index 00000000000..36e229ef8fe --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/CoroutineBuildFrameCommand.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.command + +import com.intellij.debugger.engine.DebugProcessImpl +import com.intellij.debugger.engine.JavaStackFrame +import com.intellij.debugger.engine.evaluation.EvaluationContext +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.impl.DebuggerContextImpl +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.debugger.jdi.ThreadReferenceProxyImpl +import com.intellij.debugger.memory.utils.StackFrameItem +import com.intellij.debugger.ui.impl.watch.* +import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineAsyncStackTraceProvider +import org.jetbrains.kotlin.idea.debugger.coroutines.data.* + +@Deprecated("Moved to XCoroutineView") +class CoroutineBuildFrameCommand( + node: DebuggerTreeNodeImpl, + val descriptor: CoroutineDescriptorImpl, + nodeManager: NodeManagerImpl, + debuggerContext: DebuggerContextImpl +) : BuildCoroutineNodeCommand(node, debuggerContext, nodeManager) { + + companion object { + val creationStackTraceSeparator = "\b\b\b" // the "\b\b\b" is used for creation stacktrace separator in kotlinx.coroutines + } + + override fun threadAction() { + val debugProcess = debuggerContext.debugProcess ?: return + val evalContext = debuggerContext.createEvaluationContext() ?: return + + when (descriptor.infoData.state) { + CoroutineInfoData.State.RUNNING -> { + if (renderRunningCoroutine(debugProcess, evalContext)) return + } + CoroutineInfoData.State.SUSPENDED, CoroutineInfoData.State.CREATED -> { + if (renderSuspendedCoroutine(evalContext)) return + } + } + createCreationStackTraceDescriptor(evalContext) + updateUI(true) + } + + private fun createCreationStackTraceDescriptor(evalContext: EvaluationContextImpl) { + val threadProxy = debuggerContext.suspendContext?.thread ?: return + val proxy = threadProxy.forceFrames().first() + val trace = descriptor.infoData.stackTrace + val index = trace.indexOfFirst { it.className.startsWith(creationStackTraceSeparator) } + val creationNode = myNodeManager.createNode( + CreationFramesDescriptor(trace.subList(index + 1, trace.size)), evalContext) + myChildren.add(creationNode) + trace.subList(index + 1, trace.size).forEach { + val descriptor = myNodeManager.createNode( + CoroutineCreatedStackFrameDescriptor(it, proxy), evalContext) + creationNode.add(descriptor) + } + } + + private fun renderSuspendedCoroutine(evalContext: EvaluationContextImpl): Boolean { + val threadProxy = debuggerContext.suspendContext?.thread ?: return true + val proxy = threadProxy.forceFrames().first() + // the thread is paused on breakpoint - it has at least one frame + for (it in descriptor.infoData.stackTrace) { + if (it.className.startsWith(creationStackTraceSeparator)) break + myChildren.add(createCoroutineFrameDescriptor(evalContext, it, proxy)) + } + return false + } + + private fun createCoroutineFrameDescriptor( + evalContext: EvaluationContextImpl, + frame: StackTraceElement, + proxy: StackFrameProxyImpl, + parent: NodeDescriptorImpl? = null + ): DebuggerTreeNodeImpl { + return myNodeManager.createNode( + myNodeManager.getDescriptor( + parent, + CoroutineStackTraceData(descriptor.infoData, proxy, evalContext, frame) + ), evalContext + ) + } + + private fun renderRunningCoroutine( + debugProcess: DebugProcessImpl, + evalContext: EvaluationContextImpl + ): Boolean { + if (descriptor.infoData.thread == null) { + myChildren.add(myNodeManager.createMessageNode("Frames are not available")) + return true + } + val proxy = ThreadReferenceProxyImpl( + debugProcess.virtualMachineProxy, + descriptor.infoData.thread + ) + val frames = proxy.forceFrames() + var endRange = findResumeWithMethodFrameIndex(frames) + + for (frame in 0..frames.lastIndex) { + if (frame == endRange) { + val javaStackFrame = JavaStackFrame(StackFrameDescriptorImpl(frames[endRange - 1], MethodsTracker()), true) + val async = CoroutineAsyncStackTraceProvider() + .getAsyncStackTrace(javaStackFrame, evalContext.suspendContext) + async?.forEach { + myChildren.add(createAsyncFrameDescriptor(evalContext, it, frames[frame])) + } + } else { + val frameDescriptor = createFrameDescriptor(evalContext, frames[frame]) + myChildren.add(frameDescriptor) + } + } + updateUI(true) + return false + } + + private fun findResumeWithMethodFrameIndex(frames: List) : Int { + for (j: Int in frames.lastIndex downTo 0 ) + if (isResumeMethodFrame(frames[j])) { + return j + } + return 0 + } + + private fun isResumeMethodFrame(frame: StackFrameProxyImpl) = frame.location().method().name() == "resumeWith" + + private fun createFrameDescriptor( + evalContext: EvaluationContext, + frame: StackFrameProxyImpl + ): DebuggerTreeNodeImpl { + return myNodeManager.createNode( + myNodeManager.getStackFrameDescriptor(descriptor, frame), + evalContext + ) + } + + private fun createAsyncFrameDescriptor( + evalContext: EvaluationContextImpl, + frame: StackFrameItem, + proxy: StackFrameProxyImpl + ): DebuggerTreeNodeImpl { + return myNodeManager.createNode( + myNodeManager.getDescriptor( + descriptor, + CoroutineStackFrameData(descriptor.infoData, proxy, evalContext, frame) + ), evalContext + ) + } +} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/CoroutineBuilder.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/CoroutineBuilder.kt new file mode 100644 index 00000000000..51b988c3589 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/CoroutineBuilder.kt @@ -0,0 +1,240 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.command + +import com.intellij.debugger.engine.* +import com.intellij.debugger.jdi.ClassesByNameProvider +import com.intellij.debugger.jdi.GeneratedLocation +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.debugger.jdi.ThreadReferenceProxyImpl +import com.intellij.debugger.memory.utils.StackFrameItem +import com.intellij.debugger.ui.impl.watch.MethodsTracker +import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl +import com.intellij.util.containers.ContainerUtil +import com.intellij.xdebugger.XSourcePosition +import com.intellij.xdebugger.frame.XStackFrame +import com.sun.jdi.AbsentInformationException +import com.sun.jdi.Location +import com.sun.jdi.ReferenceType +import com.sun.jdi.ThreadReference +import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineAsyncStackTraceProvider +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutines.util.getPosition + + +class CoroutineBuilder(val suspendContext: SuspendContextImpl) { + private val methodsTracker = MethodsTracker() + private val coroutineStackFrameProvider = CoroutineAsyncStackTraceProvider() + val virtualMachineProxy = suspendContext.debugProcess.virtualMachineProxy + val classesByName = ClassesByNameProvider.createCache(virtualMachineProxy.allClasses()) + + companion object { + val CREATION_STACK_TRACE_SEPARATOR = "\b\b\b" // the "\b\b\b" is used for creation stacktrace separator in kotlinx.coroutines + } + + fun build(ci: CoroutineInfoData): List { + val coroutineStackFrameList = mutableListOf() + val suspendedThreadProxy = suspendedThreadProxy() + val firstSuspendedStackFrameProxyImpl = firstSuspendedThreadFrame() + val creationFrameSeparatorIndex = findCreationFrameIndex(ci.stackTrace) + val runningThreadReferenceProxyImpl = currentRunningThreadProxy(ci.thread ?: suspendedThreadProxy().threadReference) + val positionManager = suspendContext.debugProcess.positionManager + + if (ci.state == CoroutineInfoData.State.RUNNING && runningThreadReferenceProxyImpl is ThreadReferenceProxyImpl) { + val executionStack = JavaExecutionStack(runningThreadReferenceProxyImpl, suspendContext.debugProcess, suspendContext.thread == runningThreadReferenceProxyImpl) + + val frames = runningThreadReferenceProxyImpl.forceFrames() + var resumeMethodIndex = findResumeMethodIndex(frames) + for (frameIndex in 0..frames.lastIndex) { + val runningStackFrameProxy = frames[frameIndex] + if (frameIndex == resumeMethodIndex) { + val previousFrame = frames[resumeMethodIndex - 1] + val previousJavaFrame = JavaStackFrame(StackFrameDescriptorImpl(previousFrame, methodsTracker), true) + val asyncStackTrace = coroutineStackFrameProvider + .getAsyncStackTrace(previousJavaFrame, suspendContext) + asyncStackTrace?.forEach { +// val xStackFrame = positionManager.createStackFrame(runningStackFrameProxy, suspendContext.debugProcess, it.location) + val xStackFrame = JavaStackFrame(StackFrameDescriptorImpl(previousFrame, methodsTracker), true) +// val itStackFrame = null + coroutineStackFrameList.add(AsyncCoroutineStackFrameItem(runningStackFrameProxy, "some label", it, xStackFrame)) + } + } else { +// val xStackFrame = stackFrame(positionManager, runningStackFrameProxy, runningStackFrameProxy.location()) + val xStackFrame = executionStack.createStackFrame(runningStackFrameProxy) + coroutineStackFrameList.add(RunningCoroutineStackFrameItem(runningStackFrameProxy, "", xStackFrame)) + } + } + } else if (ci.state == CoroutineInfoData.State + .SUSPENDED || runningThreadReferenceProxyImpl == null + ) { // to get frames from CoroutineInfo anyway + // the thread is paused on breakpoint - it has at least one frame + ci.stackTrace.subList(0, creationFrameSeparatorIndex).forEach { + val xStackFrame = stackFrame(positionManager, firstSuspendedStackFrameProxyImpl, it) +// val xStackFrame = null + coroutineStackFrameList.add(SuspendCoroutineStackFrameItem(firstSuspendedStackFrameProxyImpl, "suspended frame", it, xStackFrame)) + } + } + + val executionStack = JavaExecutionStack(suspendedThreadProxy(), suspendContext.debugProcess, false) + val xStackFrame = executionStack.createStackFrame(firstSuspendedStackFrameProxyImpl) + + ci.stackTrace.subList(creationFrameSeparatorIndex + 1, ci.stackTrace.size).forEach { + var location = createLocation(it) + coroutineStackFrameList.add(CreationCoroutineStackFrameItem(firstSuspendedStackFrameProxyImpl, "creation frame", it, xStackFrame, location)) + } + ci.stackFrameList.addAll(coroutineStackFrameList) + return coroutineStackFrameList + } + + fun createLocation(stackTraceElement: StackTraceElement): Location { + return findLocation( + ContainerUtil.getFirstItem(classesByName[stackTraceElement.className]), + stackTraceElement.methodName, + stackTraceElement.lineNumber + ) + } + + private fun findLocation( + type: ReferenceType?, + methodName: String, + line: Int + ): Location { + if (type != null && line >= 0) { + try { + val location = type.locationsOfLine(DebugProcess.JAVA_STRATUM, null, line).stream() + .filter { l: Location -> l.method().name() == methodName } + .findFirst().orElse(null) + if (location != null) { + return location + } + } catch (ignored: AbsentInformationException) { + } + } + return GeneratedLocation(suspendContext.debugProcess, type, methodName, line) + } + + fun stackFrame(positionManager: CompoundPositionManager, runningStackFrameProxy: StackFrameProxyImpl, location: Location): XStackFrame { + return positionManager.createStackFrame(runningStackFrameProxy, suspendContext.debugProcess, location)!! + } + + fun stackFrame(positionManager: CompoundPositionManager, runningStackFrameProxy: StackFrameProxyImpl, stackTraceElement: StackTraceElement) : XStackFrame { + val location = createLocation(stackTraceElement) +// val sourcePosition = getPosition(stackTraceElement) + return positionManager.createStackFrame(runningStackFrameProxy, suspendContext.debugProcess, location)!! + } + + /** + * Tries to find creation frame separator if any, returns last index if none found + */ + private fun findCreationFrameIndex(frames: List): Int { + var index = frames.indexOfFirst { isCreationSeparatorFrame(it) } + return if (index < 0) + frames.lastIndex + else + index + } + + private fun isCreationSeparatorFrame(it: StackTraceElement) = + it.className.startsWith(CREATION_STACK_TRACE_SEPARATOR) + + private fun firstSuspendedThreadFrame(): StackFrameProxyImpl = + suspendedThreadProxy().forceFrames().first() + + // retrieves currently suspended but active and executing corouting thread proxy + fun currentRunningThreadProxy(threadReference: ThreadReference?): ThreadReferenceProxyImpl? = + ThreadReferenceProxyImpl(suspendContext.debugProcess.virtualMachineProxy, threadReference) + + // retrieves current suspended thread proxy + fun suspendedThreadProxy(): ThreadReferenceProxyImpl = + suspendContext.thread!! // @TODO hash replace !! + + private fun findResumeMethodIndex(frames: List): Int { + for (j: Int in frames.lastIndex downTo 0) + if (isResumeMethodFrame(frames[j])) { + return j + } + return 0 + } + + private fun isResumeMethodFrame(frame: StackFrameProxyImpl) = frame.location().method().name() == "resumeWith" + + /** + * Should be invoked on manager thread + */ +/* + private fun createSyntheticStackFrame( + descriptor: SuspendStackFrameDescriptor, + pos: XSourcePosition, + project: Project + ): Pair? { + val context = DebuggerManagerEx.getInstanceEx(project).context + val suspendContext = context.suspendContext ?: return null + val proxy = suspendContext.thread ?: return null + val executionStack = JavaExecutionStack(proxy, suspendContext.debugProcess, false) + executionStack.initTopFrame() + val evalContext = context.createEvaluationContext() + val frameProxy = evalContext?.frameProxy ?: return null + val execContext = ExecutionContext(evalContext, frameProxy) + val continuation = descriptor.continuation // guaranteed that it is a BaseContinuationImpl + val aMethod = (continuation.type() as ClassType).concreteMethodByName( + "getStackTraceElement", + "()Ljava/lang/StackTraceElement;" + ) + val vars = with(CoroutineAsyncStackTraceProvider()) { + AsyncStackTraceContext( + execContext, + aMethod + ).getSpilledVariables(continuation) + } ?: return null + return executionStack to SyntheticStackFrame(descriptor, vars, pos) + } + + */ +} + +class CreationCoroutineStackFrameItem( + frame: StackFrameProxyImpl, + label: String = "", + val stackTraceElement: StackTraceElement, + stackFrame: XStackFrame, + val location: Location +) : CoroutineStackFrameItem(frame, label, stackFrame) { + override fun location() = location +} + +class SuspendCoroutineStackFrameItem( + frame: StackFrameProxyImpl, + label: String = "", + val stackTraceElement: StackTraceElement, + stackFrame: XStackFrame +) : CoroutineStackFrameItem(frame, label, stackFrame) { + override fun location() = frame.location() +} + +class AsyncCoroutineStackFrameItem( + frame: StackFrameProxyImpl, + label: String = "", + val frameItem: StackFrameItem, + stackFrame: XStackFrame +) : CoroutineStackFrameItem(frame, label, stackFrame) { + override fun location() : Location = frame.location() +} + +class RunningCoroutineStackFrameItem( + frame: StackFrameProxyImpl, + label: String = "", + stackFrame: XStackFrame +) : CoroutineStackFrameItem(frame, label, stackFrame) { + val location = frame.location() // it should be invoked in manager thread + + override fun location() = location +} + +abstract class CoroutineStackFrameItem(val frame: StackFrameProxyImpl, val label: String = "", val stackFrame: XStackFrame) { + fun sourcePosition() : XSourcePosition? = stackFrame.sourcePosition + + abstract fun location(): Location +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpAction.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/CoroutineDumpAction.kt similarity index 71% rename from idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpAction.kt rename to idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/CoroutineDumpAction.kt index ad9d74285f1..699d841dc75 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpAction.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/CoroutineDumpAction.kt @@ -3,7 +3,7 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ -package org.jetbrains.kotlin.idea.debugger.coroutines +package org.jetbrains.kotlin.idea.debugger.coroutines.command import com.intellij.debugger.DebuggerManagerEx import com.intellij.debugger.engine.events.SuspendContextCommandImpl @@ -21,11 +21,14 @@ import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.openapi.ui.MessageType import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.registry.Registry import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.text.DateFormatUtil import com.intellij.xdebugger.impl.XDebuggerManagerImpl -import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext +import org.jetbrains.kotlin.idea.KotlinBundle +import org.jetbrains.kotlin.idea.debugger.coroutines.view.CoroutineDumpPanel +import org.jetbrains.kotlin.idea.debugger.coroutines.coroutineDebuggerEnabled +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.CoroutinesDebugProbesProxy @Suppress("ComponentNotRegistered") class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate { @@ -39,28 +42,23 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate { val process = context.debugProcess ?: return process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) { override fun contextAction() { - val evalContext = context.createEvaluationContext() - val frameProxy = evalContext?.frameProxy ?: return - val execContext = ExecutionContext(evalContext, frameProxy) - val states = CoroutinesDebugProbesProxy.dumpCoroutines(execContext) - if (states.isLeft) { - logger.warn(states.left) - XDebuggerManagerImpl.NOTIFICATION_GROUP - .createNotification( - "Coroutine dump failed. See log", - MessageType.WARNING - ).notify(project) - return + val states = CoroutinesDebugProbesProxy(context.suspendContext!!).dumpCoroutines() ?: return + if (states.isOk()) { + XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification( + KotlinBundle.message("debugger.session.tab.coroutine.message.error"), + MessageType.ERROR + ).notify(project) + } else { + val f = fun() { + addCoroutineDump( + project, + states.cache, + session.xDebugSession?.ui ?: return, + session.searchScope + ) + } + ApplicationManager.getApplication().invokeLater(f, ModalityState.NON_MODAL) } - val f = fun() { - addCoroutineDump( - project, - states.get(), - session.xDebugSession?.ui ?: return, - session.searchScope - ) - } - ApplicationManager.getApplication().invokeLater(f, ModalityState.NON_MODAL) } }) } @@ -69,7 +67,7 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate { /** * Analog of [DebuggerUtilsEx.addThreadDump]. */ - fun addCoroutineDump(project: Project, coroutines: List, ui: RunnerLayoutUi, searchScope: GlobalSearchScope) { + fun addCoroutineDump(project: Project, coroutines: List, ui: RunnerLayoutUi, searchScope: GlobalSearchScope) { val consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project) consoleBuilder.filters(ExceptionFilters.getFilters(searchScope)) val consoleView = consoleBuilder.console @@ -102,7 +100,7 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate { return } val debuggerSession = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession - presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached && isCoroutineDebuggerEnabled() + presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached && coroutineDebuggerEnabled() presentation.isVisible = presentation.isEnabled } } \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/RefreshCoroutinesTreeCommand.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/RefreshCoroutinesTreeCommand.kt new file mode 100644 index 00000000000..466bbbc3232 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/command/RefreshCoroutinesTreeCommand.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.command + +import com.intellij.debugger.DebuggerInvocationUtil +import com.intellij.debugger.engine.SuspendContextImpl +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.engine.events.SuspendContextCommandImpl +import com.intellij.debugger.impl.DebuggerContextImpl +import com.intellij.debugger.impl.DebuggerSession +import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl +import com.intellij.debugger.ui.impl.watch.NodeManagerImpl +import com.intellij.openapi.ui.MessageType +import com.intellij.xdebugger.impl.XDebuggerManagerImpl +import org.jetbrains.kotlin.idea.KotlinBundle +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineDescriptorData +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.CoroutinesDebugProbesProxy +import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.ManagerThreadExecutor +import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.createEvaluationContext +import org.jetbrains.kotlin.idea.debugger.coroutines.view.CoroutinesDebuggerTree + +class RefreshCoroutinesTreeCommand( + val context: DebuggerContextImpl, + private val debuggerTree: CoroutinesDebuggerTree +) : SuspendContextCommandImpl(context.suspendContext) { + + override fun contextAction() { + val nf = debuggerTree.nodeFactory + val root = nf.defaultNode + val sc: SuspendContextImpl? = suspendContext + if (context.debuggerSession is DebuggerSession && sc is SuspendContextImpl && !sc.isResumed) { + val infoCache = CoroutinesDebugProbesProxy(sc).dumpCoroutines() + if (infoCache.isOk()) { + val evaluationContext = sc.createEvaluationContext() + + for (state in infoCache.cache) { + val descriptor = createCoroutineDescriptorNode(nf, state, evaluationContext) + root.add(descriptor) + } + setRoot(root) + } else { + debuggerTree.showMessage(KotlinBundle.message("debugger.session.tab.coroutine.message.failure")) + XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification( + KotlinBundle.message("debugger.session.tab.coroutine.message.error"), + MessageType.ERROR + ) + .notify(debuggerTree.project) + } + } else { + debuggerTree.showMessage(KotlinBundle.message("debugger.session.tab.coroutine.message.resume")) + } + } + + private fun createCoroutineDescriptorNode( + nodeFactory: NodeManagerImpl, + coroutineInfoData: CoroutineInfoData, + evaluationContext: EvaluationContextImpl + ) = + nodeFactory.createNode( + nodeFactory.getDescriptor( + null, + CoroutineDescriptorData(coroutineInfoData) + ), + evaluationContext + ) + + private fun setRoot(root: DebuggerTreeNodeImpl) { + DebuggerInvocationUtil.swingInvokeLater(debuggerTree.project) { + debuggerTree.mutableModel.setRoot(root) + debuggerTree.treeChanged() + } + } + +} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/CoroutineAsyncStackFrameItem.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/CoroutineAsyncStackFrameItem.kt new file mode 100644 index 00000000000..b6c16d8102d --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/CoroutineAsyncStackFrameItem.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.data + +import com.intellij.debugger.jdi.GeneratedLocation +import com.intellij.debugger.memory.utils.StackFrameItem +import com.intellij.xdebugger.frame.XNamedValue + +class CoroutineAsyncStackFrameItem( + val location: GeneratedLocation, + spilledVariables: List +) : StackFrameItem(location, spilledVariables) { + +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/CoroutineInfoData.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/CoroutineInfoData.kt new file mode 100644 index 00000000000..1a487558b23 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/CoroutineInfoData.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.data + +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.coroutines.command.CoroutineStackFrameItem + +/** + * Represents state of a coroutine. + * @see `kotlinx.coroutines.debug.CoroutineInfo` + */ +data class CoroutineInfoData( + val name: String, + val state: State, + val threadName: String, + val threadStatus: Int, + + val stackTrace: List, + // links to jdi.* references + val thread: ThreadReference? = null, // for suspended coroutines should be null + val frame: ObjectReference? +) { + var stackFrameList = mutableListOf() + + // @TODO for refactoring/removal + val stringStackTrace: String by lazy { + buildString { + appendln("\"$name\", state: $state") + stackTrace.forEach { + appendln("\t$it") + } + } + } + + fun isSuspended() = state == State.SUSPENDED + + fun isCreated() = state == State.CREATED + + fun isEmptyStackTrace() = stackTrace.isEmpty() + + enum class State { + RUNNING, + SUSPENDED, + CREATED + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/SyntheticStackFrame.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/SyntheticStackFrame.kt similarity index 96% rename from idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/SyntheticStackFrame.kt rename to idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/SyntheticStackFrame.kt index 8c3fd985ebd..ea3fe066f55 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/SyntheticStackFrame.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/SyntheticStackFrame.kt @@ -3,7 +3,7 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ -package org.jetbrains.kotlin.idea.debugger.coroutines +package org.jetbrains.kotlin.idea.debugger.coroutines.data import com.intellij.debugger.engine.JavaStackFrame import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/TreeNodeDescriptorImpl.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/TreeNodeDescriptorImpl.kt new file mode 100644 index 00000000000..0d4355910a3 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/TreeNodeDescriptorImpl.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.data + +import com.intellij.debugger.engine.evaluation.EvaluateException +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.impl.DebuggerUtilsEx +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.debugger.memory.utils.StackFrameItem +import com.intellij.debugger.ui.impl.watch.MessageDescriptor +import com.intellij.debugger.ui.impl.watch.MethodsTracker +import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl +import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl +import com.intellij.debugger.ui.tree.render.DescriptorLabelListener +import com.intellij.icons.AllIcons +import com.sun.jdi.ObjectReference +import javax.swing.Icon + +class CoroutineDescriptorImpl(val infoData: CoroutineInfoData) : NodeDescriptorImpl() { + lateinit var icon: Icon + + override fun getName() = infoData.name + + @Throws(EvaluateException::class) + override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener): String { + val thread = infoData.thread + val name = thread?.name()?.substringBefore(" @${infoData.name}") ?: "" + val threadState = if (thread != null) DebuggerUtilsEx.getThreadStatusText(thread.status()) else "" + return "${infoData.name}: ${infoData.state}${if (name.isNotEmpty()) " on thread \"$name\":$threadState" else ""}" + } + + override fun isExpandable() = infoData.state != CoroutineInfoData.State.CREATED + + private fun calcIcon() = when { + infoData.isSuspended() -> AllIcons.Debugger.ThreadSuspended + infoData.isCreated() -> AllIcons.Debugger.ThreadStates.Idle + else -> AllIcons.Debugger.ThreadRunning + } + + override fun setContext(context: EvaluationContextImpl?) { + icon = calcIcon() + } +} + +class CreationFramesDescriptor(val frames: List) + : MessageDescriptor("Coroutine creation stack trace", INFORMATION) { + + override fun isExpandable() = true +} + +/** + * Descriptor for suspend functions + */ +class SuspendStackFrameDescriptor( + val infoData: CoroutineInfoData, + val frame: StackTraceElement, + proxy: StackFrameProxyImpl, + val continuation: ObjectReference +) : + CoroutineStackFrameDescriptor(proxy) { + override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String { + return with(frame) { + val pack = className.substringBeforeLast(".", "") + "$methodName:$lineNumber, ${className.substringAfterLast(".")} " + + if (pack.isNotEmpty()) "{$pack}" else "" + } + } + + override fun getName() : String? = frame.methodName +} + + +/** + * For the case when no data inside frame is available + */ +class CoroutineCreatedStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) : + CoroutineStackFrameDescriptor(proxy) { + override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String { + return with(frame) { + val pack = className.substringBeforeLast(".", "") + "$methodName:$lineNumber, ${className.substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}" + } + } + + override fun getName() = null +} + +class AsyncStackFrameDescriptor(val infoData: CoroutineInfoData, val frame: StackFrameItem, proxy: StackFrameProxyImpl) : + CoroutineStackFrameDescriptor(proxy) { + override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String { + return with(frame) { + val pack = path().substringBeforeLast(".", "") + "${method()}:${line()}, ${path().substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}" + } + } + + override fun getName() = frame.method() + +} + + +open class CoroutineStackFrameDescriptor(proxy: StackFrameProxyImpl) : StackFrameDescriptorImpl(proxy, MethodsTracker()) { + override fun isExpandable() = false +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/descriptors.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/descriptors.kt new file mode 100644 index 00000000000..c5974535d49 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/data/descriptors.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.data + +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.impl.descriptors.data.DescriptorData +import com.intellij.debugger.impl.descriptors.data.DisplayKey +import com.intellij.debugger.impl.descriptors.data.SimpleDisplayKey +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.debugger.memory.utils.StackFrameItem +import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl +import com.intellij.openapi.project.Project +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.LookupContinuation +import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext + +class CoroutineStackTraceData(infoData: CoroutineInfoData, proxy: StackFrameProxyImpl, evalContext: EvaluationContextImpl, val frame: StackTraceElement) + : CoroutineStackDescriptorData(infoData, proxy, evalContext) { + + override fun hashCode() = frame.hashCode() + + override fun equals(other: Any?) = + other is CoroutineStackTraceData && frame == other.frame + + override fun createDescriptorImpl(project: Project): NodeDescriptorImpl { + val context = ExecutionContext(evalContext, proxy) + val lookupContinuation = LookupContinuation(context, frame) + + // retrieve continuation only if suspend method + val continuation = lookupContinuation.findContinuation(infoData) + + return if (continuation is ObjectReference) + SuspendStackFrameDescriptor(infoData, frame, proxy, continuation) + else + CoroutineCreatedStackFrameDescriptor(frame, proxy) + } +} + +class CoroutineStackFrameData(infoData: CoroutineInfoData, proxy: StackFrameProxyImpl, evalContext: EvaluationContextImpl, val frame: StackFrameItem) + : CoroutineStackDescriptorData(infoData, proxy, evalContext) { + override fun createDescriptorImpl(project: Project): NodeDescriptorImpl = + AsyncStackFrameDescriptor(infoData, frame, proxy) + + override fun hashCode() = frame.hashCode() + + override fun equals(other: Any?) = other is CoroutineStackFrameData && frame == other.frame +} + +abstract class CoroutineStackDescriptorData(val infoData: CoroutineInfoData, val proxy: StackFrameProxyImpl, val evalContext: EvaluationContextImpl) + : DescriptorData() { + + override fun getDisplayKey(): DisplayKey = SimpleDisplayKey(infoData) +} + +/** + * Describes coroutine itself in the tree (name: STATE), has children if stacktrace is not empty (state = CREATED) + */ +class CoroutineDescriptorData(private val infoData: CoroutineInfoData) : DescriptorData() { + + override fun createDescriptorImpl(project: Project) = + CoroutineDescriptorImpl(infoData) + + override fun equals(other: Any?) = if (other !is CoroutineDescriptorData) false else infoData.name == other.infoData.name + + override fun hashCode() = infoData.name.hashCode() + + override fun getDisplayKey(): DisplayKey = SimpleDisplayKey(infoData.name) +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/descriptors.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/descriptors.kt deleted file mode 100644 index 9b5c7e27288..00000000000 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/descriptors.kt +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -package org.jetbrains.kotlin.idea.debugger.coroutines - -import com.intellij.debugger.DebuggerManagerEx -import com.intellij.debugger.engine.SuspendContextImpl -import com.intellij.debugger.engine.evaluation.EvaluateException -import com.intellij.debugger.engine.evaluation.EvaluationContextImpl -import com.intellij.debugger.impl.DebuggerUtilsEx -import com.intellij.debugger.impl.descriptors.data.DescriptorData -import com.intellij.debugger.impl.descriptors.data.DisplayKey -import com.intellij.debugger.impl.descriptors.data.SimpleDisplayKey -import com.intellij.debugger.jdi.StackFrameProxyImpl -import com.intellij.debugger.memory.utils.StackFrameItem -import com.intellij.debugger.ui.impl.watch.MethodsTracker -import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl -import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl -import com.intellij.debugger.ui.tree.render.DescriptorLabelListener -import com.intellij.icons.AllIcons -import com.intellij.openapi.project.Project -import com.sun.jdi.ClassType -import com.sun.jdi.ObjectReference -import javaslang.control.Either -import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext -import javax.swing.Icon - -/** - * Describes coroutine itself in the tree (name: STATE), has children if stacktrace is not empty (state = CREATED) - */ -class CoroutineData(private val state: CoroutineState) : DescriptorData() { - - override fun createDescriptorImpl(project: Project): CoroutineDescriptorImpl { - return CoroutineDescriptorImpl(state) - } - - override fun equals(other: Any?) = if (other !is CoroutineData) false else state.name == other.state.name - - override fun hashCode() = state.name.hashCode() - - override fun getDisplayKey(): DisplayKey = SimpleDisplayKey(state.name) -} - -class CoroutineDescriptorImpl(val state: CoroutineState) : NodeDescriptorImpl() { - var suspendContext: SuspendContextImpl? = null - lateinit var icon: Icon - - override fun getName(): String? { - return state.name - } - - @Throws(EvaluateException::class) - override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener): String { - val name = if (state.thread != null) state.thread.name().substringBefore(" @${state.name}") else "" - val threadState = if (state.thread != null) DebuggerUtilsEx.getThreadStatusText(state.thread.status()) else "" - return "${state.name}: ${state.state}${if (name.isNotEmpty()) " on thread \"$name\":$threadState" else ""}" - } - - override fun isExpandable(): Boolean { - return state.state != CoroutineState.State.CREATED - } - - private fun calcIcon() = when { - state.isSuspended -> AllIcons.Debugger.ThreadSuspended - state.state == CoroutineState.State.CREATED -> AllIcons.Debugger.ThreadStates.Idle - else -> AllIcons.Debugger.ThreadRunning - } - - override fun setContext(context: EvaluationContextImpl?) { - icon = calcIcon() - } -} - -class CoroutineStackFrameData private constructor(val state: CoroutineState, private val proxy: StackFrameProxyImpl) : - DescriptorData() { - private lateinit var frame: Either - - constructor(state: CoroutineState, frame: StackTraceElement, proxy: StackFrameProxyImpl) : this(state, proxy) { - this.frame = Either.left(frame) - } - - constructor(state: CoroutineState, frameItem: StackFrameItem, proxy: StackFrameProxyImpl) : this(state, proxy) { - this.frame = Either.right(frameItem) - } - - override fun hashCode(): Int { - return if (frame.isLeft) frame.left.hashCode() else frame.get().hashCode() - } - - override fun equals(other: Any?): Boolean { - return other is CoroutineStackFrameData && frame == other.frame - } - - /** - * Returns [EmptyStackFrameDescriptor], [SuspendStackFrameDescriptor] - * or [AsyncStackFrameDescriptor] according to current frame - */ - override fun createDescriptorImpl(project: Project): NodeDescriptorImpl { - val isLeft = frame.isLeft - if (!isLeft) return AsyncStackFrameDescriptor(state, frame.get(), proxy) - // check whether last fun is suspend fun - val frame = frame.left - val suspendContext = - DebuggerManagerEx.getInstanceEx(project).context.suspendContext ?: return EmptyStackFrameDescriptor( - frame, - proxy - ) - val suspendProxy = suspendContext.frameProxy ?: return EmptyStackFrameDescriptor( - frame, - proxy - ) - val evalContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy) - val context = ExecutionContext(evalContext, suspendProxy) - val clazz = context.findClass(frame.className) as ClassType - val method = clazz.methodsByName(frame.methodName).last { - val loc = it.location().lineNumber() - loc < 0 && frame.lineNumber < 0 || loc > 0 && loc <= frame.lineNumber - } // pick correct method if an overloaded one is given - return if ("Lkotlin/coroutines/Continuation;)" in method.signature() || - method.name() == "invokeSuspend" && - method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;" // suspend fun or invokeSuspend - ) { - val continuation = state.getContinuation(frame, context) - if (continuation == null) EmptyStackFrameDescriptor( - frame, - proxy - ) else - SuspendStackFrameDescriptor( - state, - frame, - proxy, - continuation - ) - } else EmptyStackFrameDescriptor(frame, proxy) - } - - override fun getDisplayKey(): DisplayKey = SimpleDisplayKey(state) -} - -/** - * Descriptor for suspend functions - */ -class SuspendStackFrameDescriptor( - val state: CoroutineState, - val frame: StackTraceElement, - proxy: StackFrameProxyImpl, - val continuation: ObjectReference -) : - StackFrameDescriptorImpl(proxy, MethodsTracker()) { - override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String { - return with(frame) { - val pack = className.substringBeforeLast(".", "") - "$methodName:$lineNumber, ${className.substringAfterLast(".")} " + - if (pack.isNotEmpty()) "{$pack}" else "" - } - } - - override fun isExpandable() = false - - override fun getName(): String { - return frame.methodName - } -} - -class AsyncStackFrameDescriptor(val state: CoroutineState, val frame: StackFrameItem, proxy: StackFrameProxyImpl) : - StackFrameDescriptorImpl(proxy, MethodsTracker()) { - override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String { - return with(frame) { - val pack = path().substringBeforeLast(".", "") - "${method()}:${line()}, ${path().substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}" - } - } - - override fun getName(): String { - return frame.method() - } - - override fun isExpandable(): Boolean = false -} - -/** - * For the case when no data inside frame is available - */ -class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) : - StackFrameDescriptorImpl(proxy, MethodsTracker()) { - override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String { - return with(frame) { - val pack = className.substringBeforeLast(".", "") - "$methodName:$lineNumber, ${className.substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}" - } - } - - override fun getName() = null - override fun isExpandable() = false -} - -class CreationFramesDescriptor(val frames: List) : NodeDescriptorImpl() { - override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?) = name - - override fun setContext(context: EvaluationContextImpl?) {} - override fun getName() = "Coroutine creation stack trace" - override fun isExpandable() = true -} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/proxy/CoroutinesDebugProbesProxy.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/proxy/CoroutinesDebugProbesProxy.kt new file mode 100644 index 00000000000..821ec75eb36 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/proxy/CoroutinesDebugProbesProxy.kt @@ -0,0 +1,255 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ +package org.jetbrains.kotlin.idea.debugger.coroutines.proxy + +import com.intellij.debugger.engine.DebuggerManagerThreadImpl +import com.intellij.debugger.engine.SuspendContextImpl +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.impl.PrioritizedTask +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.xdebugger.XDebugProcess +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.coroutines.command.CoroutineBuilder +import org.jetbrains.kotlin.idea.debugger.coroutines.view.CoroutineInfoCache +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutines.util.logger +import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext + +fun SuspendContextImpl.createEvaluationContext() = + EvaluationContextImpl(this, this.frameProxy) + +class CoroutinesDebugProbesProxy(val suspendContext: SuspendContextImpl) { + private val log by logger + + // @TODO refactor to extract initialization logic + private var executionContext: ExecutionContext = ExecutionContext( + EvaluationContextImpl(suspendContext, suspendContext.frameProxy), + suspendContext.frameProxy as StackFrameProxyImpl) + // might want to use inner class but also having to monitor order of fields + private var refs: ProcessReferences = ProcessReferences(executionContext) + + companion object { + private const val DEBUG_PACKAGE = "kotlinx.coroutines.debug" + } + + @Synchronized + @Suppress("unused") + fun install() = + executionContext.invokeMethodAsVoid(refs.instance, "install") + + @Synchronized + @Suppress("unused") + fun uninstall() = + executionContext.invokeMethodAsVoid(refs.instance, "uninstall") + + /** + * Invokes DebugProbes from debugged process's classpath and returns states of coroutines + * Should be invoked on debugger manager thread + */ + @Synchronized + fun dumpCoroutines() : CoroutineInfoCache { + val coroutineInfoCache = CoroutineInfoCache() + try { + val infoList = dump() + coroutineInfoCache.ok(infoList) + } catch (e: Throwable) { + log.error("Exception is thrown by calling dumpCoroutines.", e) + coroutineInfoCache.fail() + } + return coroutineInfoCache + } + + private fun dump(): List { + val coroutinesInfo = dumpCoroutinesInfo() ?: return emptyList() + + executionContext.keepReference(coroutinesInfo) + val size = sizeOf(coroutinesInfo) + + return MutableList(size) { + val elem = getElementFromList(coroutinesInfo, it) + fetchCoroutineState(elem) + } + } + + private fun dumpCoroutinesInfo() = + executionContext.invokeMethodAsObject(refs.instance, refs.dumpMethod) + + fun frameBuilder() = CoroutineBuilder(suspendContext) + + private fun getElementFromList(instance: ObjectReference, num: Int) = + executionContext.invokeMethod( + instance, refs.getRef, + listOf(executionContext.vm.virtualMachine.mirrorOf(num)) + ) as ObjectReference + + private fun fetchCoroutineState(instance: ObjectReference) : CoroutineInfoData { + val name = getName(instance) + val state = getState(instance) + val thread = getLastObservedThread(instance, refs.lastObservedThreadFieldRef) + val lastObservedFrameFieldRef = instance.getValue(refs.lastObservedFrameFieldRef) as? ObjectReference + return CoroutineInfoData( + name, + CoroutineInfoData.State.valueOf(state), + getThreadName(instance), + getThreadState(instance), + + getStackTrace(instance), + thread, + lastObservedFrameFieldRef + ) + } + + private fun getThreadName(instance: ObjectReference) : String { + return "thread name" + } + + private fun getThreadState(instance: ObjectReference) : Int { + return 1 + } + + private fun getName( + info: ObjectReference // CoroutineInfo instance + ): String { + // equals to `coroutineInfo.context.get(CoroutineName).name` + val coroutineContextInst = executionContext.invokeMethod( + info, + refs.getContextRef, + emptyList() + ) as? ObjectReference ?: throw IllegalArgumentException("Coroutine context must not be null") + val coroutineName = executionContext.invokeMethod( + coroutineContextInst, + refs.getContextElement, listOf(refs.keyFieldValueRef) + ) as? ObjectReference + // If the coroutine doesn't have a given name, CoroutineContext.get(CoroutineName) returns null + val name = if (coroutineName != null) (executionContext.invokeMethod( + coroutineName, + refs.getNameRef, + emptyList() + ) as StringReference).value() else "coroutine" + val id = (info.getValue(refs.sequenceNumberFieldRef) as LongValue).value() + return "$name#$id" + } + + private fun getState( + info: ObjectReference // CoroutineInfo instance + ): String { + // equals to `stringState = coroutineInfo.state.toString()` + val state = executionContext.invokeMethod(info, refs.getStateRef, emptyList()) as ObjectReference + return (executionContext.invokeMethod(state, refs.toString, emptyList()) as StringReference).value() + } + + private fun getLastObservedThread( + info: ObjectReference, // CoroutineInfo instance + threadRef: Field // reference to lastObservedThread + ): ThreadReference? = info.getValue(threadRef) as? ThreadReference + + /** + * Returns list of stackTraceElements for the given CoroutineInfo's [ObjectReference] + */ + private fun getStackTrace( + info: ObjectReference + ): List { + val frameList = lastObservedStackTrace(info) + val tmpList = mutableListOf() + for(it in 0 until sizeOf(frameList)) { + val frame = getElementFromList(frameList, it) + val ste = newStackTraceElement(frame) + tmpList.add(ste) + } + val mergedFrameList = enhanceStackTraceWithThreadDump(listOf(info, frameList)) + val size = sizeOf(mergedFrameList) + + val list = mutableListOf() + + for (it in 0 until size) { + val frame = getElementFromList(mergedFrameList, it) + val ste = newStackTraceElement(frame) + list.add(// 0, // add in the beginning // @TODO what's the point? + ste) + } + return list + } + + private fun newStackTraceElement(frame: ObjectReference) = + StackTraceElement( + fetchClassName(frame), + fetchMethodName(frame), + fetchFileName(frame), + fetchLine(frame) + ) + + private fun fetchLine(instance: ObjectReference) = + (instance.getValue(refs.lineNumberFieldRef) as? IntegerValue)?.value() ?: -1 + + private fun fetchFileName(instance: ObjectReference) = + (instance.getValue(refs.fileNameFieldRef) as? StringReference)?.value() ?: "" + + private fun fetchMethodName(instance: ObjectReference) = + (instance.getValue(refs.methodNameFieldRef) as? StringReference)?.value() ?: "" + + private fun fetchClassName(instance: ObjectReference) = + (instance.getValue(refs.declaringClassFieldRef) as? StringReference)?.value() ?: "" + + private fun lastObservedStackTrace(instance: ObjectReference) = + executionContext.invokeMethod(instance, refs.lastObservedStackTraceRef, emptyList()) as ObjectReference + + private fun enhanceStackTraceWithThreadDump(args: List) = + executionContext.invokeMethod( + refs.debugProbesImplInstance, + refs.enhanceStackTraceWithThreadDumpRef, args) as ObjectReference + + private fun sizeOf(args: ObjectReference): Int = + (executionContext.invokeMethod(args, refs.sizeRef, emptyList()) as IntegerValue).value() + + /** + * @TODO refactor later + * Holds ClassTypes, Methods, ObjectReferences and Fields for a particular jvm + */ + class ProcessReferences(executionContext: ExecutionContext) { + // kotlinx.coroutines.debug.DebugProbes instance and methods + val debugProbesClsRef = executionContext.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType + val debugProbesImplClsRef = executionContext.findClass("$DEBUG_PACKAGE.internal.DebugProbesImpl") as ClassType + val coroutineNameClsRef = executionContext.findClass("kotlinx.coroutines.CoroutineName") as ClassType + val classClsRef = executionContext.findClass("java.lang.Object") as ClassType + val debugProbesImplInstance = with(debugProbesImplClsRef) { getValue(fieldByName("INSTANCE")) as ObjectReference } + val enhanceStackTraceWithThreadDumpRef: Method = debugProbesImplClsRef + .methodsByName("enhanceStackTraceWithThreadDump").single() + + val dumpMethod: Method = debugProbesClsRef.concreteMethodByName("dumpCoroutinesInfo", "()Ljava/util/List;") + val instance = with(debugProbesClsRef) { getValue(fieldByName("INSTANCE")) as ObjectReference } + + // CoroutineInfo + val coroutineInfoClsRef = executionContext.findClass("$DEBUG_PACKAGE.CoroutineInfo") as ClassType + val coroutineContextClsRef = executionContext.findClass("kotlin.coroutines.CoroutineContext") as InterfaceType + + val getStateRef: Method = coroutineInfoClsRef.concreteMethodByName("getState", "()Lkotlinx/coroutines/debug/State;") + val getContextRef: Method = coroutineInfoClsRef.concreteMethodByName("getContext", "()Lkotlin/coroutines/CoroutineContext;") + val sequenceNumberFieldRef: Field = coroutineInfoClsRef.fieldByName("sequenceNumber") + val lastObservedStackTraceRef: Method = coroutineInfoClsRef.methodsByName("lastObservedStackTrace").single() + val getContextElement: Method = coroutineContextClsRef.methodsByName("get").single() + val getNameRef: Method = coroutineNameClsRef.methodsByName("getName").single() + val keyFieldRef = coroutineNameClsRef.fieldByName("Key") + val toString: Method = classClsRef.concreteMethodByName("toString", "()Ljava/lang/String;") + + val lastObservedThreadFieldRef: Field = coroutineInfoClsRef.fieldByName("lastObservedThread") + val lastObservedFrameFieldRef: Field = coroutineInfoClsRef.fieldByName("lastObservedFrame") // continuation + + // Methods for list + val listClsRef = executionContext.findClass("java.util.List") as InterfaceType + val sizeRef: Method = listClsRef.methodsByName("size").single() + val getRef: Method = listClsRef.methodsByName("get").single() + val stackTraceElementClsRef = executionContext.findClass("java.lang.StackTraceElement") as ClassType + + // for StackTraceElement + val methodNameFieldRef: Field = stackTraceElementClsRef.fieldByName("methodName") + val declaringClassFieldRef: Field = stackTraceElementClsRef.fieldByName("declaringClass") + val fileNameFieldRef: Field = stackTraceElementClsRef.fieldByName("fileName") + val lineNumberFieldRef: Field = stackTraceElementClsRef.fieldByName("lineNumber") + + // value + val keyFieldValueRef = coroutineNameClsRef.getValue(keyFieldRef) as ObjectReference + + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineState.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/proxy/LookupContinuation.kt similarity index 67% rename from idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineState.kt rename to idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/proxy/LookupContinuation.kt index 27092447771..81505c409d5 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineState.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/proxy/LookupContinuation.kt @@ -3,52 +3,43 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ -package org.jetbrains.kotlin.idea.debugger.coroutines +package org.jetbrains.kotlin.idea.debugger.coroutines.proxy import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext import org.jetbrains.kotlin.idea.debugger.isSubtype -/** - * Represents state of a coroutine. - * @see `kotlinx.coroutines.debug.CoroutineInfo` - */ -class CoroutineState( - val name: String, - val state: State, - val thread: ThreadReference? = null, - val stackTrace: List, - val frame: ObjectReference? -) { - val isSuspended: Boolean = state == State.SUSPENDED - val isEmptyStackTrace: Boolean by lazy { stackTrace.isEmpty() } - val stringStackTrace: String by lazy { - buildString { - appendln("\"$name\", state: $state") - stackTrace.forEach { - appendln("\t$it") - } - } +class LookupContinuation(val context: ExecutionContext, val frame: StackTraceElement) { + + private fun suspendOrInvokeSuspend(method: Method): Boolean = + "Lkotlin/coroutines/Continuation;)" in method.signature() || + (method.name() == "invokeSuspend" && method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;") // suspend fun or invokeSuspend + + private fun findMethod() : Method { + val clazz = context.findClass(frame.className) as ClassType + val method = clazz.methodsByName(frame.methodName).last { + val loc = it.location().lineNumber() + loc < 0 && frame.lineNumber < 0 || loc > 0 && loc <= frame.lineNumber + } // pick correct method if an overloaded one is given + return method + } + + fun isApplicable(): Boolean { + val method = findMethod() + return suspendOrInvokeSuspend(method) } /** - * Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl) - * @return null if given ObjectReference is not a BaseContinuationImpl instance or completion is null - */ - private fun getNextFrame(continuation: ObjectReference, context: ExecutionContext): ObjectReference? { - val type = continuation.type() as ClassType - if (!type.isSubtype("kotlin.coroutines.jvm.internal.BaseContinuationImpl")) return null - val next = type.concreteMethodByName("getCompletion", "()Lkotlin/coroutines/Continuation;") - return context.invokeMethod(continuation, next, emptyList()) as? ObjectReference - } - - /** - * Find continuation for the [stackTraceElement] + * Find continuation for the [frame] * Gets current CoroutineInfo.lastObservedFrame and finds next frames in it until null or needed stackTraceElement is found * @return null if matching continuation is not found or is not BaseContinuationImpl */ - fun getContinuation(stackTraceElement: StackTraceElement, context: ExecutionContext): ObjectReference? { - var continuation = frame ?: return null + fun findContinuation(infoData: CoroutineInfoData): ObjectReference? { + if (!isApplicable()) + return null + + var continuation = infoData.frame ?: return null val baseType = "kotlin.coroutines.jvm.internal.BaseContinuationImpl" val getTrace = (continuation.type() as ClassType).concreteMethodByName( "getStackTraceElement", @@ -71,7 +62,7 @@ class CoroutineState( } while (continuation.type().isSubtype(baseType) - && (stackTraceElement.className != className() || stackTraceElement.lineNumber != lineNumber()) + && (frame.className != className() || frame.lineNumber != lineNumber()) ) { // while continuation is BaseContinuationImpl and it's frame equals to the current continuation = getNextFrame(continuation, context) ?: return null @@ -79,9 +70,15 @@ class CoroutineState( return if (continuation.type().isSubtype(baseType)) continuation else null } - enum class State { - RUNNING, - SUSPENDED, - CREATED + + /** + * Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl) + * @return null if given ObjectReference is not a BaseContinuationImpl instance or completion is null + */ + private fun getNextFrame(continuation: ObjectReference, context: ExecutionContext): ObjectReference? { + val type = continuation.type() as ClassType + if (!type.isSubtype("kotlin.coroutines.jvm.internal.BaseContinuationImpl")) return null + val next = type.concreteMethodByName("getCompletion", "()Lkotlin/coroutines/Continuation;") + return context.invokeMethod(continuation, next, emptyList()) as? ObjectReference } } \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/proxy/ManagerThreadExecutor.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/proxy/ManagerThreadExecutor.kt new file mode 100644 index 00000000000..aef1fe3ad1a --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/proxy/ManagerThreadExecutor.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.proxy + +import com.intellij.debugger.engine.DebugProcessImpl +import com.intellij.debugger.engine.SuspendContextImpl +import com.intellij.debugger.engine.events.DebuggerCommandImpl +import com.intellij.debugger.impl.PrioritizedTask +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.util.Computable + +class ManagerThreadExecutor(val debugProcess: DebugProcessImpl, val priority: PrioritizedTask.Priority = PrioritizedTask.Priority.NORMAL) { + fun schedule(f: () -> Unit) { + val runnable = object : Runnable { + override fun run() {f()} + } + debugProcess.managerThread.schedule(priority, runnable) + } + + fun schedule(f: DebuggerCommandImpl) { + debugProcess.managerThread.schedule(f) + } +} + +class ApplicationThreadExecutor() { + fun invoke(f: () -> T) : T { + return ApplicationManager.getApplication().runReadAction(Computable(f)) + } +} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/util/KUtils.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/util/KUtils.kt new file mode 100644 index 00000000000..4c66a7a7d90 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/util/KUtils.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.util + +import com.intellij.debugger.DebuggerManagerEx +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.debugger.memory.utils.StackFrameItem +import com.intellij.debugger.ui.impl.watch.MethodsTracker +import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl +import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Computable +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.xdebugger.XDebuggerUtil +import com.intellij.xdebugger.XSourcePosition +import com.sun.jdi.ClassType +import javaslang.control.Either +import org.jetbrains.kotlin.idea.debugger.coroutines.data.AsyncStackFrameDescriptor +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutines.data.SuspendStackFrameDescriptor +import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.ApplicationThreadExecutor +import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.LookupContinuation +import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext + +fun getPosition(stackTraceElement: StackTraceElement, project: Project): XSourcePosition? { + val psiFacade = JavaPsiFacade.getInstance(project) + + val psiClass = ApplicationThreadExecutor().invoke { + @Suppress("DEPRECATION") + psiFacade.findClass( + stackTraceElement.className.substringBefore("$"), // find outer class, for which psi exists TODO + GlobalSearchScope.everythingScope(project)) + } + + val classFile = psiClass?.containingFile?.virtualFile + // to convert to 0-based line number or '-1' to do not move + val lineNumber = if (stackTraceElement.lineNumber > 0) stackTraceElement.lineNumber - 1 else return null + return XDebuggerUtil.getInstance().createPosition(classFile, lineNumber) +} + + +/* + +private fun buildSuspendStackFrameChildren(descriptor: SuspendStackFrameDescriptor, frame: StackTraceElement, project: Project) { + val context = DebuggerManagerEx.getInstanceEx(project).context + val pos = getPosition(frame) ?: return + context.debugProcess?.managerThread?.schedule(object : SuspendContextCommandImpl(context.suspendContext) { + override fun contextAction() { + val (stack, stackFrame) = createSyntheticStackFrame(descriptor, pos) ?: return + val action: () -> Unit = { context.debuggerSession?.xDebugSession?.setCurrentStackFrame(stack, stackFrame) } + ApplicationManager.getApplication() + .invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree)) + } + }) +} + +private fun buildAsyncStackFrameChildren(descriptor: AsyncStackFrameDescriptor, process: DebugProcessImpl) { + process.managerThread?.schedule(object : DebuggerCommandImpl() { + override fun action() { + val context = DebuggerManagerEx.getInstanceEx(project).context + val proxy = ThreadReferenceProxyImpl( + process.virtualMachineProxy, + descriptor.state.thread // is not null because it's a running coroutine + ) + val executionStack = JavaExecutionStack(proxy, process, false) + executionStack.initTopFrame() + val frame = descriptor.frame.createFrame(process) + DebuggerUIUtil.invokeLater { + context.debuggerSession?.xDebugSession?.setCurrentStackFrame( + executionStack, + frame + ) + } + } + }) +} + +private fun buildEmptyStackFrameChildren(descriptor: EmptyStackFrameDescriptor, project: Project) { + val position = getPosition(descriptor.frame) ?: return + val context = DebuggerManagerEx.getInstanceEx(project).context + val suspendContext = context.suspendContext ?: return + val proxy = suspendContext.thread ?: return + context.debugProcess?.managerThread?.schedule(object : DebuggerCommandImpl() { + override fun action() { + val executionStack = + JavaExecutionStack(proxy, context.debugProcess!!, false) + executionStack.initTopFrame() + val frame = SyntheticStackFrame(descriptor, emptyList(), position) + val action: () -> Unit = + { context.debuggerSession?.xDebugSession?.setCurrentStackFrame(executionStack, frame) } + ApplicationManager.getApplication() + .invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree)) + } + }) +} + +*/ + +class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) : + StackFrameDescriptorImpl(proxy, MethodsTracker()) { +} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/util/Providers.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/util/Providers.kt new file mode 100644 index 00000000000..e1b4dc6487c --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/util/Providers.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.util + +import com.intellij.openapi.diagnostic.Logger +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebugSessionListener +import javax.swing.Icon +import javax.swing.JComponent +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +interface CreateContentParamsProvider { + fun createContentParams() : CreateContentParams +} + +data class CreateContentParams(val id: String, val component: JComponent, val displayName: String, val icon: Icon?, val parentComponent: JComponent) + + +interface XDebugSessionListenerProvider { + fun debugSessionListener(session: XDebugSession) : XDebugSessionListener +} + +/** + * Logger instantiation sample: 'val log by logger' + */ +val logger: ReadOnlyProperty get() = LoggerDelegate() + +class LoggerDelegate : ReadOnlyProperty { + lateinit var logger: Logger + + override fun getValue(thisRef: Any, property: KProperty<*>): Logger { + if (!::logger.isInitialized) + logger = Logger.getInstance(thisRef.javaClass) + return logger + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/util/Utils.java b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/util/Utils.java new file mode 100644 index 00000000000..edab9a5ebbd --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/util/Utils.java @@ -0,0 +1,35 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.util; + +import com.intellij.xdebugger.XSourcePosition; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class Utils { + public static Object callPrivate( + Object instance, + String methodName, + Object... args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + + Class[] classes = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + if(args[i] instanceof ReflectionSpecificType) + classes[i] = ((ReflectionSpecificType) args[i]).specificClass(); + else + classes[i] = args[i].getClass(); + } + Method method = instance.getClass().getDeclaredMethod(methodName, classes); + method.setAccessible(true); + Object r = method.invoke(instance, args); + return r; + } + + public interface ReflectionSpecificType { + Class specificClass(); + } +} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpPanel.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/CoroutineDumpPanel.kt similarity index 89% rename from idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpPanel.kt rename to idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/CoroutineDumpPanel.kt index 38dcedf7959..3b82351de12 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpPanel.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/CoroutineDumpPanel.kt @@ -3,7 +3,7 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ -package org.jetbrains.kotlin.idea.debugger.coroutines +package org.jetbrains.kotlin.idea.debugger.coroutines.view import com.intellij.codeInsight.highlighting.HighlightManager import com.intellij.execution.ui.ConsoleView @@ -32,6 +32,7 @@ import com.intellij.ui.components.JBList import com.intellij.unscramble.AnalyzeStacktraceUtil import com.intellij.util.PlatformIcons import com.intellij.util.ui.EmptyIcon +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData import java.awt.BorderLayout import java.awt.Color import java.awt.datatransfer.StringSelection @@ -42,10 +43,10 @@ import javax.swing.event.DocumentEvent /** * Panel with dump of coroutines */ -class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActions: DefaultActionGroup, val dump: List) : +class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActions: DefaultActionGroup, val dump: List) : JPanel(BorderLayout()), DataProvider { private var exporterToTextFile: ExporterToTextFile - private var mergedDump = ArrayList() + private var mergedDump = ArrayList() val filterField = SearchTextField() val filterPanel = JPanel(BorderLayout()) private val coroutinesList = JBList(DefaultListModel()) @@ -71,7 +72,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi addListSelectionListener { val index = selectedIndex if (index >= 0) { - val selection = model.getElementAt(index) as CoroutineState + val selection = model.getElementAt(index) as CoroutineInfoData AnalyzeStacktraceUtil.printStacktrace(consoleView, selection.stringStackTrace) } else { AnalyzeStacktraceUtil.printStacktrace(consoleView, "") @@ -80,7 +81,8 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi } } - exporterToTextFile = MyToFileExporter(project, dump) + exporterToTextFile = + MyToFileExporter(project, dump) val filterAction = FilterAction().apply { registerCustomShortcutSet( @@ -90,7 +92,9 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi } toolbarActions.apply { add(filterAction) - add(CopyToClipboardAction(dump, project)) + add( + CopyToClipboardAction(dump, project) + ) add(ActionManager.getInstance().getAction(IdeActions.ACTION_EXPORT_TO_TEXT_FILE)) add(MergeStackTracesAction()) } @@ -174,18 +178,18 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi override fun getData(dataId: String): Any? = if (PlatformDataKeys.EXPORTER_TO_TEXT_FILE.`is`(dataId)) exporterToTextFile else null - private fun getCoroutineStateIcon(state: CoroutineState): Icon { - return when (state.state) { - CoroutineState.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign) - CoroutineState.State.SUSPENDED -> AllIcons.Actions.Pause + private fun getCoroutineStateIcon(infoData: CoroutineInfoData): Icon { + return when (infoData.state) { + CoroutineInfoData.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign) + CoroutineInfoData.State.SUSPENDED -> AllIcons.Actions.Pause else -> EmptyIcon.create(6) } } - private fun getAttributes(state: CoroutineState): SimpleTextAttributes { + private fun getAttributes(infoData: CoroutineInfoData): SimpleTextAttributes { return when { - state.isSuspended -> SimpleTextAttributes.GRAY_ATTRIBUTES - state.isEmptyStackTrace -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter()) + infoData.isSuspended() -> SimpleTextAttributes.GRAY_ATTRIBUTES + infoData.isEmptyStackTrace() -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter()) else -> SimpleTextAttributes.REGULAR_ATTRIBUTES } @@ -194,7 +198,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi private inner class CoroutineListCellRenderer : ColoredListCellRenderer() { override fun customizeCellRenderer(list: JList<*>, value: Any, index: Int, selected: Boolean, hasFocus: Boolean) { - val state = value as CoroutineState + val state = value as CoroutineInfoData icon = getCoroutineStateIcon(state) val attrs = getAttributes(state) append(state.name + " (", attrs) @@ -246,7 +250,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi } } - private class CopyToClipboardAction(private val myCoroutinesDump: List, private val myProject: Project) : + private class CopyToClipboardAction(private val myCoroutinesDump: List, private val myProject: Project) : DumbAwareAction("Copy to Clipboard", "Copy whole coroutine dump to clipboard", PlatformIcons.COPY_ICON) { override fun actionPerformed(e: AnActionEvent) { @@ -268,17 +272,17 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi private class MyToFileExporter( private val myProject: Project, - private val states: List + private val infoData: List ) : ExporterToTextFile { override fun getReportText() = buildString { - for (state in states) + for (state in infoData) append(state.stringStackTrace).append("\n\n") } override fun getDefaultFilePath() = (myProject.basePath ?: "") + File.separator + defaultReportFileName - override fun canExport() = states.isNotEmpty() + override fun canExport() = infoData.isNotEmpty() private val defaultReportFileName = "coroutines_report.txt" } diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/CoroutineViewDebugSessionListener.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/CoroutineViewDebugSessionListener.kt new file mode 100644 index 00000000000..5bb9509f17d --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/CoroutineViewDebugSessionListener.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.view + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebugSessionListener +import com.intellij.xdebugger.frame.XSuspendContext +import com.intellij.xdebugger.impl.ui.DebuggerUIUtil +import org.jetbrains.kotlin.idea.debugger.coroutines.util.logger + +class CoroutineViewDebugSessionListener( + private val session: XDebugSession, + private val xCoroutineView: XCoroutineView +) : XDebugSessionListener { + val log by logger + + override fun sessionPaused() { + val suspendContext = session.suspendContext ?: return requestClear() + xCoroutineView.forceClear() + renew(suspendContext) + } + + override fun sessionResumed() { + val suspendContext = session.suspendContext ?: return requestClear() + log.warn("sessionResumed ${session}") + renew(suspendContext) + } + + override fun sessionStopped() { + val suspendContext = session.suspendContext ?: return requestClear() + log.warn("sessionStopped ${session}") + renew(suspendContext) + } + + override fun stackFrameChanged() { + val suspendContext = session.suspendContext ?: return requestClear() + log.warn("stackFrameChanged ${session}") + renew(suspendContext) + } + + override fun beforeSessionResume() { + log.warn("beforeSessionResume ${session}") + } + + override fun settingsChanged() { + val suspendContext = session.suspendContext ?: return requestClear() + log.warn("settingsChanged ${session}") + renew(suspendContext) + } + + fun renew(suspendContext: XSuspendContext) { + DebuggerUIUtil.invokeLater { + xCoroutineView.panel.tree.setRoot(xCoroutineView.createRoot(suspendContext), false) + } + } + + fun requestClear() { + if (ApplicationManager.getApplication().isUnitTestMode) { // no delay in tests + xCoroutineView.clear() + } else { + xCoroutineView.alarm.cancelAndRequest() + } + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/CoroutinesDebuggerTree.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/CoroutinesDebuggerTree.kt new file mode 100644 index 00000000000..ccbb0e9569e --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/CoroutinesDebuggerTree.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.view + +import com.intellij.debugger.DebuggerBundle +import com.intellij.debugger.engine.events.DebuggerCommandImpl +import com.intellij.debugger.impl.DebuggerContextImpl +import com.intellij.debugger.impl.DebuggerSession +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.debugger.ui.impl.ThreadsDebuggerTree +import com.intellij.debugger.ui.impl.watch.* +import com.intellij.debugger.ui.tree.StackFrameDescriptor +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import org.jetbrains.kotlin.idea.debugger.coroutines.command.CoroutineBuildCreationFrameCommand +import org.jetbrains.kotlin.idea.debugger.coroutines.command.CoroutineBuildFrameCommand +import org.jetbrains.kotlin.idea.debugger.coroutines.command.RefreshCoroutinesTreeCommand +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineDescriptorImpl +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CreationFramesDescriptor +import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.ManagerThreadExecutor +import org.jetbrains.kotlin.idea.debugger.coroutines.util.logger +import java.util.* + +/** + * Tree of coroutines for [CoroutinesPanel] + */ +class CoroutinesDebuggerTree(project: Project) : ThreadsDebuggerTree(project) { + private val log by logger + + // called on every step/frame + override fun build(context: DebuggerContextImpl) { + val session = context.debuggerSession + + val command = RefreshCoroutinesTreeCommand(context, this) + val debuggerSessionState = session?.state ?: DebuggerSession.State.DISPOSED + + if (ApplicationManager.getApplication().isUnitTestMode || debuggerSessionState in EnumSet.of(DebuggerSession.State.PAUSED, DebuggerSession.State.RUNNING)) { + showMessage(MessageDescriptor.EVALUATING) + ManagerThreadExecutor(context.debugProcess!!).schedule(command) + } else { + showMessage(session?.stateDescription ?: DebuggerBundle.message("status.debug.stopped")) + } + } + + + override fun getBuildNodeCommand(node: DebuggerTreeNodeImpl): DebuggerCommandImpl? { + return when(val descriptor = node.descriptor) { + is CoroutineDescriptorImpl -> + CoroutineBuildFrameCommand(node, descriptor, myNodeManager, debuggerContext) + is CreationFramesDescriptor -> + CoroutineBuildCreationFrameCommand(node, descriptor, myNodeManager, debuggerContext) + else -> null + } + } + + override fun createNodeManager(project: Project): NodeManagerImpl { + return object : NodeManagerImpl(project, this) { + override fun getContextKey(frame: StackFrameProxyImpl?): String? { + return "CoroutinesView" + } + } + } + + override fun isExpandable(node: DebuggerTreeNodeImpl): Boolean { + val descriptor = node.descriptor + return if (descriptor is StackFrameDescriptor) false else descriptor.isExpandable + } +} + +class CoroutineInfoCache(val cache: MutableList = mutableListOf(), var state: CacheState = CacheState.INIT +) { + fun ok(infoList: List) { + cache.clear() + cache.addAll(infoList) + state = CacheState.OK + } + + fun fail() { + cache.clear() + state = CacheState.FAIL + } + + fun isOk() : Boolean { + return state == CacheState.OK + } +} + +enum class CacheState() { + OK,FAIL,INIT +} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/CoroutinesPanel.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/CoroutinesPanel.kt new file mode 100644 index 00000000000..70fa557c55b --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/CoroutinesPanel.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:Suppress("DEPRECATION") + +package org.jetbrains.kotlin.idea.debugger.coroutines.view + +import com.intellij.debugger.actions.DebuggerActions +import com.intellij.debugger.impl.DebuggerStateManager +import com.intellij.debugger.ui.impl.ThreadsPanel +import com.intellij.debugger.ui.impl.watch.DebuggerTree +import com.intellij.openapi.actionSystem.* +import com.intellij.openapi.project.Project +import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineDebuggerActions + +/** + * Added into ui in [CoroutineProjectConnectionListener.registerCoroutinesPanel] + */ +class CoroutinesPanel(project: Project, stateManager: DebuggerStateManager) : ThreadsPanel(project, stateManager) { + + override fun createTreeView(): DebuggerTree { + return CoroutinesDebuggerTree(project) + } + + override fun createPopupMenu(): ActionPopupMenu { + val group = ActionManager.getInstance().getAction(DebuggerActions.THREADS_PANEL_POPUP) as DefaultActionGroup + return ActionManager.getInstance().createActionPopupMenu( + CoroutineDebuggerActions.COROUTINE_PANEL_POPUP, group) + } + + override fun getData(dataId: String): Any? { + return if (helpDataId(dataId)) HELP_ID else super.getData(dataId) + } + + private fun helpDataId(dataId: String): Boolean = PlatformDataKeys.HELP_ID.`is`(dataId) + + companion object { + val HELP_ID = "debugging.debugCoroutines" + } + +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/XCoroutineView.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/XCoroutineView.kt new file mode 100644 index 00000000000..7eb04dec26b --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/view/XCoroutineView.kt @@ -0,0 +1,263 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutines.view + +import com.intellij.debugger.engine.DebugProcessImpl +import com.intellij.debugger.engine.JavaDebugProcess +import com.intellij.debugger.engine.SuspendContextImpl +import com.intellij.debugger.impl.DebuggerUtilsEx +import com.intellij.debugger.settings.ThreadsViewSettings +import com.intellij.icons.AllIcons +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.ui.OnePixelSplitter +import com.intellij.ui.SimpleColoredComponent +import com.intellij.ui.SimpleColoredText +import com.intellij.ui.SimpleTextAttributes +import com.intellij.util.SingleAlarm +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XSourcePosition +import com.intellij.xdebugger.frame.* +import com.intellij.xdebugger.frame.presentation.XRegularValuePresentation +import com.intellij.xdebugger.frame.presentation.XValuePresentation +import com.intellij.xdebugger.impl.ui.DebuggerUIUtil +import com.intellij.xdebugger.impl.ui.XDebuggerUIConstants +import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree +import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreePanel +import com.intellij.xdebugger.impl.ui.tree.nodes.XValueContainerNode +import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl +import com.sun.jdi.Location +import com.sun.jdi.ReferenceType +import org.jetbrains.kotlin.idea.KotlinBundle +import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineDebuggerContentInfo +import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineDebuggerContentInfo.Companion.XCOROUTINE_POPUP_ACTION_GROUP +import org.jetbrains.kotlin.idea.debugger.coroutines.command.CoroutineStackFrameItem +import org.jetbrains.kotlin.idea.debugger.coroutines.command.CreationCoroutineStackFrameItem +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.CoroutinesDebugProbesProxy +import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.ManagerThreadExecutor +import org.jetbrains.kotlin.idea.debugger.coroutines.util.CreateContentParams +import org.jetbrains.kotlin.idea.debugger.coroutines.util.CreateContentParamsProvider +import org.jetbrains.kotlin.idea.debugger.coroutines.util.XDebugSessionListenerProvider +import org.jetbrains.kotlin.idea.debugger.coroutines.util.logger +import javax.swing.Icon + + +class XCoroutineView(val project: Project, val session: XDebugSession) : + Disposable, XDebugSessionListenerProvider, CreateContentParamsProvider { + val log by logger + val splitter = OnePixelSplitter("SomeKey", 0.25f) + val panel = XDebuggerTreePanel(project, session.debugProcess.editorsProvider, this, null, XCOROUTINE_POPUP_ACTION_GROUP, null) + val alarm = SingleAlarm(Runnable { clear() }, VIEW_CLEAR_DELAY, this) + val debugProcess: DebugProcessImpl = (session.debugProcess as JavaDebugProcess).debuggerSession.process + + companion object { + private val VIEW_CLEAR_DELAY = 100 //ms + } + + init { + splitter.firstComponent = panel.mainPanel + } + + fun clear() { + DebuggerUIUtil.invokeLater { + panel.tree + .setRoot(object : XValueContainerNode(panel.tree, null, true, object : XValueContainer() {}) {}, false) + } + } + + override fun dispose() { + } + + fun forceClear() { + alarm.cancel() + clear() + } + + fun createRoot(suspendContext: XSuspendContext) = + XCoroutinesRootNode(panel.tree, suspendContext) + + override fun debugSessionListener(session: XDebugSession) = + CoroutineViewDebugSessionListener(session, this) + + override fun createContentParams(): CreateContentParams = + CreateContentParams( + CoroutineDebuggerContentInfo.XCOROUTINE_THREADS_CONTENT, + splitter, + KotlinBundle.message("debugger.session.tab.xcoroutine.title"), + null, + panel.tree + ) + +} + +class XCoroutinesRootNode(tree: XDebuggerTree, suspendContext: XSuspendContext) : + XValueContainerNode(tree, null, false, CoroutineGroupContainer(suspendContext)) { +} + +class CoroutineGroupContainer( + val suspendContext: XSuspendContext +) : XValueContainer() { + override fun computeChildren(node: XCompositeNode) { + val children = XValueChildrenList() + children.add("", CoroutineContainer(suspendContext as SuspendContextImpl, "Default group")) + node.addChildren(children, true) + } +} + +class CoroutineContainer( + val suspendContext: SuspendContextImpl, + val groupName: String +) : XValue() { + override fun computeChildren(node: XCompositeNode) { + val managerThreadExecutor = ManagerThreadExecutor(suspendContext.debugProcess) + managerThreadExecutor.schedule { + val debugProbesProxy = CoroutinesDebugProbesProxy(suspendContext) + + var coroutineCache = debugProbesProxy.dumpCoroutines() + if(coroutineCache.isOk()) { + val children = XValueChildrenList() + coroutineCache.cache.forEach { + children.add("", FramesContainer(it, debugProbesProxy, managerThreadExecutor)) + } + node.addChildren(children, true) + } else + node.addChildren(XValueChildrenList.EMPTY, true) + } + } + + override fun computePresentation(node: XValueNode, place: XValuePlace) { + node.setPresentation(AllIcons.Debugger.ThreadGroup, XRegularValuePresentation(groupName, null, ""), true) + } +} + +class FramesContainer( + private val coroutineInfoData: CoroutineInfoData, + private val debugProbesProxy: CoroutinesDebugProbesProxy, + private val managerThreadExecutor: ManagerThreadExecutor +) : XValue() { + override fun computeChildren(node: XCompositeNode) { + managerThreadExecutor.schedule { + val children = XValueChildrenList() + debugProbesProxy.frameBuilder().build(coroutineInfoData) + val creationStack = mutableListOf() + coroutineInfoData.stackFrameList.forEach { + val frameValue = when (it) { + is CreationCoroutineStackFrameItem -> { + creationStack.add(it) + null + } + else -> CoroutineFrameValue(it) + } + frameValue?.let { + children.add("", frameValue) + } + } + children.add("", CreationFramesContainer(creationStack)) + node.addChildren(children, true) + } + } + + override fun computePresentation(node: XValueNode, place: XValuePlace) { + val icon = when (coroutineInfoData.state) { + CoroutineInfoData.State.SUSPENDED -> AllIcons.Debugger.ThreadSuspended + CoroutineInfoData.State.RUNNING -> AllIcons.Debugger.ThreadRunning + CoroutineInfoData.State.CREATED -> AllIcons.Debugger.ThreadStates.Idle + } + + val valuePresentation = customizePresentation(coroutineInfoData) + node.setPresentation(icon, valuePresentation, true) + } + + private fun customizePresentation(coroutineInfoData: CoroutineInfoData): XRegularValuePresentation { + val component = SimpleColoredComponent() + val thread = coroutineInfoData.thread + val name = thread?.name()?.substringBefore(" @${coroutineInfoData.name}") ?: "" + val threadState = if (thread != null) DebuggerUtilsEx.getThreadStatusText(thread.status()) else "" + + component.append("\"").append(coroutineInfoData.name, XDebuggerUIConstants.VALUE_NAME_ATTRIBUTES) + component.append("\": ${coroutineInfoData.state} ${if (name.isNotEmpty()) "on thread \"$name\":$threadState" else ""}") + return XRegularValuePresentation(component.getCharSequence(false).toString(), null, "") + } +} + +class CreationFramesContainer(val creationFrames: List) : XValue() { + override fun computeChildren(node: XCompositeNode) { + val children = XValueChildrenList() + + creationFrames.forEach { + children.add("", CoroutineFrameValue(it)) + } + node.addChildren(children, true) + } + + override fun computePresentation(node: XValueNode, place: XValuePlace) { + node.setPresentation(AllIcons.Debugger.ThreadSuspended, XRegularValuePresentation("Creation stack frame", null, ""), true) + } +} + +class CoroutineFrameValue(val frame: CoroutineStackFrameItem) : XValue() { + override fun computePresentation(node: XValueNode, place: XValuePlace) { + val presentation = customizePresentation(frame) + if(node is XValueNodeImpl) { + node.setPresentation(null, XRegularValuePresentation("", null, ""), false) + presentation.texts.forEachIndexed { i, s -> + node.text.append(s, presentation.attributes[i]) + } + } else { + val component = SimpleColoredComponent() + presentation.appendToComponent(component) + val valuePresentation = XRegularValuePresentation(component.getCharSequence(false).toString(), null, "") + node.setPresentation(null, valuePresentation, false) + } + } +} + +fun customizePresentation(frame: CoroutineStackFrameItem) : SimpleColoredText = + calcRepresentation(frame.location(), ThreadsViewSettings.getInstance()) + + +/** + * Taken from #StackFrameDescriptorImpl.calcRepresentation + */ +fun calcRepresentation(location: Location, settings: ThreadsViewSettings): SimpleColoredText { + val label = SimpleColoredText() + DebuggerUIUtil.getColorScheme(null) + if (location.method() != null) { + val myName = location.method().name() + label.append(if (settings.SHOW_ARGUMENTS_TYPES) DebuggerUtilsEx.methodNameWithArguments(location.method()) else myName, XDebuggerUIConstants.VALUE_NAME_ATTRIBUTES) + } + if (settings.SHOW_LINE_NUMBER) { + label.append(":", SimpleTextAttributes.REGULAR_ATTRIBUTES) + label.append("" + DebuggerUtilsEx.getLineNumber(location, false), SimpleTextAttributes.REGULAR_ATTRIBUTES) + } + if (settings.SHOW_CLASS_NAME) { + val name: String? + name = try { + val refType: ReferenceType = location.declaringType() + refType?.name() + } catch (e: InternalError) { + e.toString() + } + if (name != null) { + label.append(", ", SimpleTextAttributes.REGULAR_ATTRIBUTES) + val dotIndex = name.lastIndexOf('.') + if (dotIndex < 0) { + label.append(name, SimpleTextAttributes.REGULAR_ATTRIBUTES) + } else { + label.append(name.substring(dotIndex + 1), SimpleTextAttributes.REGULAR_ATTRIBUTES) + if (settings.SHOW_PACKAGE_NAME) { + label.append(" (${name.substring( 0, dotIndex)})", SimpleTextAttributes.REGULAR_ATTRIBUTES) + } + } + } + } + if (settings.SHOW_SOURCE_NAME) { + label.append(", ", SimpleTextAttributes.REGULAR_ATTRIBUTES) + label.append(DebuggerUtilsEx.getSourceName(location) { e: Throwable? -> "Unknown Source" }, SimpleTextAttributes.REGULAR_ATTRIBUTES) + } + return label +} diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/KotlinStackFrame.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/KotlinStackFrame.kt index a94ec376e02..f7d94c30470 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/KotlinStackFrame.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/stackFrame/KotlinStackFrame.kt @@ -222,6 +222,15 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe @Suppress("ConvertToStringTemplate") return name().startsWith(AsmUtil.LABELED_THIS_PARAMETER) } + + override fun hashCode(): Int { + return super.hashCode() + } + + override fun equals(other: Any?) : Boolean { + val eq = super.equals(other) + return other is KotlinStackFrame && eq + } } interface ThisLocalVariable { diff --git a/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractAsyncStackTraceTest.kt b/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractAsyncStackTraceTest.kt index b4d891793a7..6f36cbd1337 100644 --- a/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractAsyncStackTraceTest.kt +++ b/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractAsyncStackTraceTest.kt @@ -10,7 +10,7 @@ import com.intellij.debugger.engine.JavaValue import com.intellij.debugger.memory.utils.StackFrameItem import com.intellij.execution.process.ProcessOutputTypes import com.intellij.openapi.extensions.Extensions -import org.jetbrains.kotlin.idea.debugger.KotlinCoroutinesAsyncStackTraceProvider +import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineAsyncStackTraceProvider import org.jetbrains.kotlin.idea.debugger.test.preference.DebuggerPreferences import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull import org.jetbrains.kotlin.utils.getSafe @@ -54,7 +54,7 @@ abstract class AbstractAsyncStackTraceTest : KotlinDescriptorTestCaseWithSteppin } } - private fun getAsyncStackTraceProvider(): KotlinCoroutinesAsyncStackTraceProvider? { + private fun getAsyncStackTraceProvider(): CoroutineAsyncStackTraceProvider? { val area = Extensions.getArea(null) if (!area.hasExtensionPoint(ASYNC_STACKTRACE_EP_NAME)) { System.err.println("$ASYNC_STACKTRACE_EP_NAME extension point is not found (probably old IDE version)") @@ -62,7 +62,7 @@ abstract class AbstractAsyncStackTraceTest : KotlinDescriptorTestCaseWithSteppin } val extensionPoint = area.getExtensionPoint(ASYNC_STACKTRACE_EP_NAME) - val provider = extensionPoint.extensions.firstIsInstanceOrNull() + val provider = extensionPoint.extensions.firstIsInstanceOrNull() if (provider == null) { System.err.println("Kotlin coroutine async stack trace provider is not found") diff --git a/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractCoroutineDumpTest.kt b/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractCoroutineDumpTest.kt index 91927b824a7..b964ae65831 100644 --- a/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractCoroutineDumpTest.kt +++ b/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractCoroutineDumpTest.kt @@ -5,59 +5,53 @@ package org.jetbrains.kotlin.idea.debugger.test -import com.intellij.debugger.engine.evaluation.EvaluationContextImpl import com.intellij.execution.configurations.JavaParameters import com.intellij.execution.process.ProcessOutputTypes import com.intellij.jarRepository.JarRepositoryManager import com.intellij.jarRepository.RemoteRepositoryDescription import org.jetbrains.idea.maven.aether.ArtifactKind import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor -import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineState -import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutinesDebugProbesProxy -import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext +import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.CoroutinesDebugProbesProxy import org.jetbrains.kotlin.idea.debugger.test.preference.DebuggerPreferences abstract class AbstractCoroutineDumpTest : KotlinDescriptorTestCaseWithStepping() { override fun doMultiFileTest(files: TestFiles, preferences: DebuggerPreferences) { doOnBreakpoint { - val evalContext = EvaluationContextImpl(this, frameProxy) - val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint) - val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext) + val infoCache = CoroutinesDebugProbesProxy(this).dumpCoroutines() try { - if (either.isRight) + if (infoCache.isOk()) try { - val states = either.get() + val states = infoCache.cache print(stringDump(states), ProcessOutputTypes.SYSTEM) } catch (ignored: Throwable) { } else - throw AssertionError("Dump failed", either.left) + throw AssertionError("Dump failed") } finally { resume(this) } } doOnBreakpoint { - val evalContext = EvaluationContextImpl(this, frameProxy) - val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint) - val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext) + val infoCache = CoroutinesDebugProbesProxy(this).dumpCoroutines() try { - if (either.isRight) + if (infoCache.isOk()) try { - val states = either.get() + val states = infoCache.cache print(stringDump(states), ProcessOutputTypes.SYSTEM) } catch (ignored: Throwable) { } else - throw AssertionError("Dump failed", either.left) + throw AssertionError("Dump failed") } finally { resume(this) } } } - private fun stringDump(states: List) = buildString { - states.forEach { + private fun stringDump(infoData: List) = buildString { + infoData.forEach { appendln("\"${it.name}\", state: ${it.state}") } } diff --git a/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/evaluate/ExecutionContext.kt b/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/evaluate/ExecutionContext.kt index 44d9d4d6ee1..a4ed3e1c8a3 100644 --- a/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/evaluate/ExecutionContext.kt +++ b/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/evaluate/ExecutionContext.kt @@ -18,6 +18,7 @@ import com.intellij.debugger.jdi.VirtualMachineProxyImpl import com.intellij.openapi.project.Project import com.sun.jdi.* import com.sun.jdi.request.EventRequest +import org.jetbrains.kotlin.idea.debugger.hopelessAware import org.jetbrains.org.objectweb.asm.Type class ExecutionContext(val evaluationContext: EvaluationContextImpl, val frameProxy: StackFrameProxyImpl) { @@ -100,4 +101,65 @@ class ExecutionContext(val evaluationContext: EvaluationContextImpl, val framePr @Suppress("DEPRECATION") DebuggerUtilsEx.keep(reference, evaluationContext) } -} \ No newline at end of file + + fun findClassSafe(className: String): ClassType? = + hopelessAware { findClass(className) as? ClassType } + + + fun invokeMethodAsString(instance: ObjectReference, methodName: String): String? = + (findAndInvoke(instance, instance.referenceType(), methodName, "()Ljava/lang/String;") as? StringReference)?.value() ?: null + + fun invokeMethodAsInt(instance: ObjectReference, methodName: String): Int? = + (findAndInvoke(instance, instance.referenceType(), methodName, "()I") as? IntegerValue)?.value() ?: null + + fun invokeMethodAsObject(type: ClassType, methodName: String, vararg params: Value): ObjectReference? = + invokeMethodAsObject(type, methodName, null, *params) + + fun invokeMethodAsObject(type: ClassType, methodName: String, methodSignature: String?, vararg params: Value): ObjectReference? = + findAndInvoke(type, methodName, methodSignature, *params) as? ObjectReference + + fun invokeMethodAsObject(instance: ObjectReference, methodName: String, vararg params: Value): ObjectReference? = + invokeMethodAsObject(instance, methodName, null, *params) + + fun invokeMethodAsObject(instance: ObjectReference, methodName: String, methodSignature: String?, vararg params: Value): ObjectReference? = + findAndInvoke(instance, methodName, methodSignature, *params) as? ObjectReference + + fun invokeMethodAsObject(instance: ObjectReference, method: Method, vararg params: Value): ObjectReference? = + invokeMethod(instance, method, params.asList()) as? ObjectReference + + fun invokeMethodAsVoid(type: ClassType, methodName: String, methodSignature: String? = null, vararg params: Value = emptyArray()) = + findAndInvoke(type, methodName, methodSignature, *params) + + fun invokeMethodAsVoid(instance: ObjectReference, methodName: String, methodSignature: String? = null, vararg params: Value = emptyArray()) = + findAndInvoke(instance, methodName, methodSignature, *params) + + fun invokeMethodAsArray(instance: ClassType, methodName: String, methodSignature: String, vararg params: Value): ArrayReference? = + findAndInvoke(instance, methodName, methodSignature, *params) as? ArrayReference + + private fun findAndInvoke(ref: ObjectReference, type: ReferenceType, name: String, methodSignature: String, vararg params: Value): Value? { + val method = type.methodsByName(name, methodSignature).single() + return invokeMethod(ref, method, params.asList()) + } + + /** + * static method invocation + */ + fun findAndInvoke(type: ClassType, name: String, methodSignature: String? = null, vararg params: Value): Value? { + val method = + if (methodSignature is String) + type.methodsByName(name, methodSignature).single() + else + type.methodsByName(name).single() + return invokeMethod(type, method, params.asList()) + } + + fun findAndInvoke(instance: ObjectReference, name: String, methodSignature: String? = null, vararg params: Value): Value? { + val type = instance.referenceType() + val method = + if (methodSignature is String) + type.methodsByName(name, methodSignature).single() + else + type.methodsByName(name).single() + return invokeMethod(instance, method, params.asList()) + } +} diff --git a/idea/resources/META-INF/jvm.xml b/idea/resources/META-INF/jvm.xml index 37ba495e926..c06617aaf3d 100644 --- a/idea/resources/META-INF/jvm.xml +++ b/idea/resources/META-INF/jvm.xml @@ -57,7 +57,7 @@ @@ -90,7 +90,7 @@ - +