[coroutine][debugger] refactoring being done
X-view approach added for review, thread groups added
This commit is contained in:
committed by
Vladimir Ilmov
parent
7f0437da68
commit
61c5ef61cc
@@ -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.modification.label=Field &modification
|
||||||
debugger.field.watchpoints.properties.panel.field.initialization.label=Field &initialization
|
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 Lint
|
||||||
android.klint.inspections.group.name=Android Lint for Kotlin
|
android.klint.inspections.group.name=Android Lint for Kotlin
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -28,7 +28,7 @@ class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtensi
|
|||||||
val lines = arrayOf(
|
val lines = arrayOf(
|
||||||
"gradle.taskGraph.beforeTask { Task task ->",
|
"gradle.taskGraph.beforeTask { Task task ->",
|
||||||
" if (task instanceof Test) {",
|
" 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)",
|
" if (kotlinxCoroutinesDebugJar)",
|
||||||
" task.jvmArgs (\"-javaagent:\${kotlinxCoroutinesDebugJar?.absolutePath}\", \"-ea\")",
|
" task.jvmArgs (\"-javaagent:\${kotlinxCoroutinesDebugJar?.absolutePath}\", \"-ea\")",
|
||||||
" }",
|
" }",
|
||||||
@@ -38,5 +38,6 @@ class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtensi
|
|||||||
initScriptConsumer.consume(script)
|
initScriptConsumer.consume(script)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// supposed to be the same as [CoroutineProjectConnectionListener.kt].coroutineDebuggerEnabled
|
||||||
private fun coroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines")
|
private fun coroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines")
|
||||||
}
|
}
|
||||||
-152
@@ -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<StackFrameItem>? {
|
|
||||||
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<StackFrameItem>? {
|
|
||||||
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<StackFrameItem>? {
|
|
||||||
val frames = mutableListOf<StackFrameItem>()
|
|
||||||
collectFramesRecursively(continuation, frames)
|
|
||||||
return frames
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun collectFramesRecursively(continuation: ObjectReference, consumer: MutableList<StackFrameItem>) {
|
|
||||||
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<XNamedValue>? {
|
|
||||||
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<XNamedValue>(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-65
@@ -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<StackFrameItem>? {
|
|
||||||
return hopelessAware { getAsyncStackTraceSafe(stackFrame.stackFrameProxy, suspendContext) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAsyncStackTraceSafe(frameProxy: StackFrameProxyImpl, suspendContext: SuspendContextImpl): List<StackFrameItem>? {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-15
@@ -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<StackFrameItem>?
|
|
||||||
}
|
|
||||||
+146
@@ -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<CoroutineAsyncStackFrameItem> {
|
||||||
|
val continuation = locateContinuation() ?: return emptyList()
|
||||||
|
val frames = mutableListOf<CoroutineAsyncStackFrameItem>()
|
||||||
|
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<CoroutineAsyncStackFrameItem>) {
|
||||||
|
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<XNamedValue>? {
|
||||||
|
|
||||||
|
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<XNamedValue>(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+53
@@ -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<CoroutineAsyncStackFrameItem>? {
|
||||||
|
val stackFrameList = hopelessAware { getAsyncStackTraceSafe(stackFrame.stackFrameProxy, suspendContext) } ?: emptyList()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAsyncStackTraceSafe(frameProxy: StackFrameProxyImpl, suspendContext: SuspendContext): List<CoroutineAsyncStackFrameItem> {
|
||||||
|
val defaultResult = emptyList<CoroutineAsyncStackFrameItem>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+23
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+170
@@ -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<Project, CoroutineProjectConnectionListener> {
|
||||||
|
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")
|
||||||
|
}
|
||||||
-105
@@ -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 <T : RunConfigurationBase<*>?> 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-13
@@ -9,31 +9,22 @@ import com.intellij.execution.configurations.DebuggingRunnerData
|
|||||||
import com.intellij.execution.configurations.JavaParameters
|
import com.intellij.execution.configurations.JavaParameters
|
||||||
import com.intellij.execution.configurations.RunConfigurationBase
|
import com.intellij.execution.configurations.RunConfigurationBase
|
||||||
import com.intellij.execution.configurations.RunnerSettings
|
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")
|
@Suppress("IncompatibleAPI")
|
||||||
class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() {
|
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 <T : RunConfigurationBase<*>?> updateJavaParameters(
|
override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
|
||||||
configuration: T,
|
configuration: T,
|
||||||
params: JavaParameters?,
|
params: JavaParameters?,
|
||||||
runnerSettings: RunnerSettings?
|
runnerSettings: RunnerSettings?
|
||||||
) {
|
) {
|
||||||
if (!isCoroutineDebuggerEnabled()) return
|
if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) {
|
||||||
if (runnerSettings is DebuggingRunnerData) {
|
configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings)
|
||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-13
@@ -9,31 +9,22 @@ import com.intellij.execution.configurations.DebuggingRunnerData
|
|||||||
import com.intellij.execution.configurations.JavaParameters
|
import com.intellij.execution.configurations.JavaParameters
|
||||||
import com.intellij.execution.configurations.RunConfigurationBase
|
import com.intellij.execution.configurations.RunConfigurationBase
|
||||||
import com.intellij.execution.configurations.RunnerSettings
|
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")
|
@Suppress("IncompatibleAPI")
|
||||||
class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() {
|
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 <T : RunConfigurationBase<*>?> updateJavaParameters(
|
override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
|
||||||
configuration: T,
|
configuration: T,
|
||||||
params: JavaParameters,
|
params: JavaParameters,
|
||||||
runnerSettings: RunnerSettings?
|
runnerSettings: RunnerSettings?
|
||||||
) {
|
) {
|
||||||
if (!isCoroutineDebuggerEnabled()) return
|
if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) {
|
||||||
if (runnerSettings is DebuggingRunnerData) {
|
configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings)
|
||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-193
@@ -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<ProcessReferences>("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<Throwable, List<CoroutineState>> {
|
|
||||||
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<CoroutineInfo>.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<StackTraceElement> {
|
|
||||||
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<StackTraceElement>()
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-451
@@ -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<DebuggerTreeNodeImpl>()
|
|
||||||
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<XExecutionStack, SyntheticStackFrame>? {
|
|
||||||
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<DebuggerTreeNodeImpl>,
|
|
||||||
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<WeakReference<SuspendContextImpl>, Either<Throwable, List<CoroutineState>>>
|
|
||||||
-162
@@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
+36
@@ -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<DebuggerTreeNodeImpl>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -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<StackFrameProxyImpl>) : 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+240
@@ -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<CoroutineStackFrameItem> {
|
||||||
|
val coroutineStackFrameList = mutableListOf<CoroutineStackFrameItem>()
|
||||||
|
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<StackTraceElement>): 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<StackFrameProxyImpl>): 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<XExecutionStack, SyntheticStackFrame>? {
|
||||||
|
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
|
||||||
|
}
|
||||||
+24
-26
@@ -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.
|
* 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.DebuggerManagerEx
|
||||||
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
|
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.project.Project
|
||||||
import com.intellij.openapi.ui.MessageType
|
import com.intellij.openapi.ui.MessageType
|
||||||
import com.intellij.openapi.util.Disposer
|
import com.intellij.openapi.util.Disposer
|
||||||
import com.intellij.openapi.util.registry.Registry
|
|
||||||
import com.intellij.psi.search.GlobalSearchScope
|
import com.intellij.psi.search.GlobalSearchScope
|
||||||
import com.intellij.util.text.DateFormatUtil
|
import com.intellij.util.text.DateFormatUtil
|
||||||
import com.intellij.xdebugger.impl.XDebuggerManagerImpl
|
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")
|
@Suppress("ComponentNotRegistered")
|
||||||
class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
|
class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
|
||||||
@@ -39,28 +42,23 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
|
|||||||
val process = context.debugProcess ?: return
|
val process = context.debugProcess ?: return
|
||||||
process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
|
process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
|
||||||
override fun contextAction() {
|
override fun contextAction() {
|
||||||
val evalContext = context.createEvaluationContext()
|
val states = CoroutinesDebugProbesProxy(context.suspendContext!!).dumpCoroutines() ?: return
|
||||||
val frameProxy = evalContext?.frameProxy ?: return
|
if (states.isOk()) {
|
||||||
val execContext = ExecutionContext(evalContext, frameProxy)
|
XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(
|
||||||
val states = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
|
KotlinBundle.message("debugger.session.tab.coroutine.message.error"),
|
||||||
if (states.isLeft) {
|
MessageType.ERROR
|
||||||
logger.warn(states.left)
|
).notify(project)
|
||||||
XDebuggerManagerImpl.NOTIFICATION_GROUP
|
} else {
|
||||||
.createNotification(
|
val f = fun() {
|
||||||
"Coroutine dump failed. See log",
|
addCoroutineDump(
|
||||||
MessageType.WARNING
|
project,
|
||||||
).notify(project)
|
states.cache,
|
||||||
return
|
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].
|
* Analog of [DebuggerUtilsEx.addThreadDump].
|
||||||
*/
|
*/
|
||||||
fun addCoroutineDump(project: Project, coroutines: List<CoroutineState>, ui: RunnerLayoutUi, searchScope: GlobalSearchScope) {
|
fun addCoroutineDump(project: Project, coroutines: List<CoroutineInfoData>, ui: RunnerLayoutUi, searchScope: GlobalSearchScope) {
|
||||||
val consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project)
|
val consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project)
|
||||||
consoleBuilder.filters(ExceptionFilters.getFilters(searchScope))
|
consoleBuilder.filters(ExceptionFilters.getFilters(searchScope))
|
||||||
val consoleView = consoleBuilder.console
|
val consoleView = consoleBuilder.console
|
||||||
@@ -102,7 +100,7 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val debuggerSession = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession
|
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
|
presentation.isVisible = presentation.isEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+78
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+17
@@ -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<XNamedValue>
|
||||||
|
) : StackFrameItem(location, spilledVariables) {
|
||||||
|
|
||||||
|
}
|
||||||
+49
@@ -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<StackTraceElement>,
|
||||||
|
// links to jdi.* references
|
||||||
|
val thread: ThreadReference? = null, // for suspended coroutines should be null
|
||||||
|
val frame: ObjectReference?
|
||||||
|
) {
|
||||||
|
var stackFrameList = mutableListOf<CoroutineStackFrameItem>()
|
||||||
|
|
||||||
|
// @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
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -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.
|
* 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.engine.JavaStackFrame
|
||||||
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
|
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
|
||||||
+107
@@ -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<StackTraceElement>)
|
||||||
|
: 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
|
||||||
|
}
|
||||||
+71
@@ -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<NodeDescriptorImpl>() {
|
||||||
|
|
||||||
|
override fun getDisplayKey(): DisplayKey<NodeDescriptorImpl> = 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<CoroutineDescriptorImpl>() {
|
||||||
|
|
||||||
|
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<CoroutineDescriptorImpl> = SimpleDisplayKey(infoData.name)
|
||||||
|
}
|
||||||
-205
@@ -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<CoroutineDescriptorImpl>() {
|
|
||||||
|
|
||||||
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<CoroutineDescriptorImpl> = 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<NodeDescriptorImpl>() {
|
|
||||||
private lateinit var frame: Either<StackTraceElement, StackFrameItem>
|
|
||||||
|
|
||||||
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<NodeDescriptorImpl> = 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<StackTraceElement>) : 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
|
|
||||||
}
|
|
||||||
+255
@@ -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<CoroutineInfoData> {
|
||||||
|
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<StackTraceElement> {
|
||||||
|
val frameList = lastObservedStackTrace(info)
|
||||||
|
val tmpList = mutableListOf<StackTraceElement>()
|
||||||
|
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<StackTraceElement>()
|
||||||
|
|
||||||
|
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<ObjectReference>) =
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-40
@@ -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.
|
* 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 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.evaluate.ExecutionContext
|
||||||
import org.jetbrains.kotlin.idea.debugger.isSubtype
|
import org.jetbrains.kotlin.idea.debugger.isSubtype
|
||||||
|
|
||||||
/**
|
class LookupContinuation(val context: ExecutionContext, val frame: StackTraceElement) {
|
||||||
* Represents state of a coroutine.
|
|
||||||
* @see `kotlinx.coroutines.debug.CoroutineInfo`
|
private fun suspendOrInvokeSuspend(method: Method): Boolean =
|
||||||
*/
|
"Lkotlin/coroutines/Continuation;)" in method.signature() ||
|
||||||
class CoroutineState(
|
(method.name() == "invokeSuspend" && method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;") // suspend fun or invokeSuspend
|
||||||
val name: String,
|
|
||||||
val state: State,
|
private fun findMethod() : Method {
|
||||||
val thread: ThreadReference? = null,
|
val clazz = context.findClass(frame.className) as ClassType
|
||||||
val stackTrace: List<StackTraceElement>,
|
val method = clazz.methodsByName(frame.methodName).last {
|
||||||
val frame: ObjectReference?
|
val loc = it.location().lineNumber()
|
||||||
) {
|
loc < 0 && frame.lineNumber < 0 || loc > 0 && loc <= frame.lineNumber
|
||||||
val isSuspended: Boolean = state == State.SUSPENDED
|
} // pick correct method if an overloaded one is given
|
||||||
val isEmptyStackTrace: Boolean by lazy { stackTrace.isEmpty() }
|
return method
|
||||||
val stringStackTrace: String by lazy {
|
}
|
||||||
buildString {
|
|
||||||
appendln("\"$name\", state: $state")
|
fun isApplicable(): Boolean {
|
||||||
stackTrace.forEach {
|
val method = findMethod()
|
||||||
appendln("\t$it")
|
return suspendOrInvokeSuspend(method)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl)
|
* Find continuation for the [frame]
|
||||||
* @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]
|
|
||||||
* Gets current CoroutineInfo.lastObservedFrame and finds next frames in it until null or needed stackTraceElement is found
|
* 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
|
* @return null if matching continuation is not found or is not BaseContinuationImpl
|
||||||
*/
|
*/
|
||||||
fun getContinuation(stackTraceElement: StackTraceElement, context: ExecutionContext): ObjectReference? {
|
fun findContinuation(infoData: CoroutineInfoData): ObjectReference? {
|
||||||
var continuation = frame ?: return null
|
if (!isApplicable())
|
||||||
|
return null
|
||||||
|
|
||||||
|
var continuation = infoData.frame ?: return null
|
||||||
val baseType = "kotlin.coroutines.jvm.internal.BaseContinuationImpl"
|
val baseType = "kotlin.coroutines.jvm.internal.BaseContinuationImpl"
|
||||||
val getTrace = (continuation.type() as ClassType).concreteMethodByName(
|
val getTrace = (continuation.type() as ClassType).concreteMethodByName(
|
||||||
"getStackTraceElement",
|
"getStackTraceElement",
|
||||||
@@ -71,7 +62,7 @@ class CoroutineState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (continuation.type().isSubtype(baseType)
|
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
|
// while continuation is BaseContinuationImpl and it's frame equals to the current
|
||||||
continuation = getNextFrame(continuation, context) ?: return null
|
continuation = getNextFrame(continuation, context) ?: return null
|
||||||
@@ -79,9 +70,15 @@ class CoroutineState(
|
|||||||
return if (continuation.type().isSubtype(baseType)) continuation else null
|
return if (continuation.type().isSubtype(baseType)) continuation else null
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class State {
|
|
||||||
RUNNING,
|
/**
|
||||||
SUSPENDED,
|
* Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl)
|
||||||
CREATED
|
* @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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+32
@@ -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 <T> invoke(f: () -> T) : T {
|
||||||
|
return ApplicationManager.getApplication().runReadAction(Computable(f))
|
||||||
|
}
|
||||||
|
}
|
||||||
+107
@@ -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()) {
|
||||||
|
}
|
||||||
+40
@@ -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<Any, Logger> get() = LoggerDelegate()
|
||||||
|
|
||||||
|
class LoggerDelegate : ReadOnlyProperty<Any, Logger> {
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
|
override fun getValue(thisRef: Any, property: KProperty<*>): Logger {
|
||||||
|
if (!::logger.isInitialized)
|
||||||
|
logger = Logger.getInstance(thisRef.javaClass)
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-18
@@ -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.
|
* 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.codeInsight.highlighting.HighlightManager
|
||||||
import com.intellij.execution.ui.ConsoleView
|
import com.intellij.execution.ui.ConsoleView
|
||||||
@@ -32,6 +32,7 @@ import com.intellij.ui.components.JBList
|
|||||||
import com.intellij.unscramble.AnalyzeStacktraceUtil
|
import com.intellij.unscramble.AnalyzeStacktraceUtil
|
||||||
import com.intellij.util.PlatformIcons
|
import com.intellij.util.PlatformIcons
|
||||||
import com.intellij.util.ui.EmptyIcon
|
import com.intellij.util.ui.EmptyIcon
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import java.awt.datatransfer.StringSelection
|
import java.awt.datatransfer.StringSelection
|
||||||
@@ -42,10 +43,10 @@ import javax.swing.event.DocumentEvent
|
|||||||
/**
|
/**
|
||||||
* Panel with dump of coroutines
|
* Panel with dump of coroutines
|
||||||
*/
|
*/
|
||||||
class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActions: DefaultActionGroup, val dump: List<CoroutineState>) :
|
class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActions: DefaultActionGroup, val dump: List<CoroutineInfoData>) :
|
||||||
JPanel(BorderLayout()), DataProvider {
|
JPanel(BorderLayout()), DataProvider {
|
||||||
private var exporterToTextFile: ExporterToTextFile
|
private var exporterToTextFile: ExporterToTextFile
|
||||||
private var mergedDump = ArrayList<CoroutineState>()
|
private var mergedDump = ArrayList<CoroutineInfoData>()
|
||||||
val filterField = SearchTextField()
|
val filterField = SearchTextField()
|
||||||
val filterPanel = JPanel(BorderLayout())
|
val filterPanel = JPanel(BorderLayout())
|
||||||
private val coroutinesList = JBList(DefaultListModel<Any>())
|
private val coroutinesList = JBList(DefaultListModel<Any>())
|
||||||
@@ -71,7 +72,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
|
|||||||
addListSelectionListener {
|
addListSelectionListener {
|
||||||
val index = selectedIndex
|
val index = selectedIndex
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
val selection = model.getElementAt(index) as CoroutineState
|
val selection = model.getElementAt(index) as CoroutineInfoData
|
||||||
AnalyzeStacktraceUtil.printStacktrace(consoleView, selection.stringStackTrace)
|
AnalyzeStacktraceUtil.printStacktrace(consoleView, selection.stringStackTrace)
|
||||||
} else {
|
} else {
|
||||||
AnalyzeStacktraceUtil.printStacktrace(consoleView, "")
|
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 {
|
val filterAction = FilterAction().apply {
|
||||||
registerCustomShortcutSet(
|
registerCustomShortcutSet(
|
||||||
@@ -90,7 +92,9 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
|
|||||||
}
|
}
|
||||||
toolbarActions.apply {
|
toolbarActions.apply {
|
||||||
add(filterAction)
|
add(filterAction)
|
||||||
add(CopyToClipboardAction(dump, project))
|
add(
|
||||||
|
CopyToClipboardAction(dump, project)
|
||||||
|
)
|
||||||
add(ActionManager.getInstance().getAction(IdeActions.ACTION_EXPORT_TO_TEXT_FILE))
|
add(ActionManager.getInstance().getAction(IdeActions.ACTION_EXPORT_TO_TEXT_FILE))
|
||||||
add(MergeStackTracesAction())
|
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
|
override fun getData(dataId: String): Any? = if (PlatformDataKeys.EXPORTER_TO_TEXT_FILE.`is`(dataId)) exporterToTextFile else null
|
||||||
|
|
||||||
private fun getCoroutineStateIcon(state: CoroutineState): Icon {
|
private fun getCoroutineStateIcon(infoData: CoroutineInfoData): Icon {
|
||||||
return when (state.state) {
|
return when (infoData.state) {
|
||||||
CoroutineState.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign)
|
CoroutineInfoData.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign)
|
||||||
CoroutineState.State.SUSPENDED -> AllIcons.Actions.Pause
|
CoroutineInfoData.State.SUSPENDED -> AllIcons.Actions.Pause
|
||||||
else -> EmptyIcon.create(6)
|
else -> EmptyIcon.create(6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAttributes(state: CoroutineState): SimpleTextAttributes {
|
private fun getAttributes(infoData: CoroutineInfoData): SimpleTextAttributes {
|
||||||
return when {
|
return when {
|
||||||
state.isSuspended -> SimpleTextAttributes.GRAY_ATTRIBUTES
|
infoData.isSuspended() -> SimpleTextAttributes.GRAY_ATTRIBUTES
|
||||||
state.isEmptyStackTrace -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter())
|
infoData.isEmptyStackTrace() -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter())
|
||||||
else -> SimpleTextAttributes.REGULAR_ATTRIBUTES
|
else -> SimpleTextAttributes.REGULAR_ATTRIBUTES
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -194,7 +198,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
|
|||||||
private inner class CoroutineListCellRenderer : ColoredListCellRenderer<Any>() {
|
private inner class CoroutineListCellRenderer : ColoredListCellRenderer<Any>() {
|
||||||
|
|
||||||
override fun customizeCellRenderer(list: JList<*>, value: Any, index: Int, selected: Boolean, hasFocus: Boolean) {
|
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)
|
icon = getCoroutineStateIcon(state)
|
||||||
val attrs = getAttributes(state)
|
val attrs = getAttributes(state)
|
||||||
append(state.name + " (", attrs)
|
append(state.name + " (", attrs)
|
||||||
@@ -246,7 +250,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CopyToClipboardAction(private val myCoroutinesDump: List<CoroutineState>, private val myProject: Project) :
|
private class CopyToClipboardAction(private val myCoroutinesDump: List<CoroutineInfoData>, private val myProject: Project) :
|
||||||
DumbAwareAction("Copy to Clipboard", "Copy whole coroutine dump to clipboard", PlatformIcons.COPY_ICON) {
|
DumbAwareAction("Copy to Clipboard", "Copy whole coroutine dump to clipboard", PlatformIcons.COPY_ICON) {
|
||||||
|
|
||||||
override fun actionPerformed(e: AnActionEvent) {
|
override fun actionPerformed(e: AnActionEvent) {
|
||||||
@@ -268,17 +272,17 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
|
|||||||
|
|
||||||
private class MyToFileExporter(
|
private class MyToFileExporter(
|
||||||
private val myProject: Project,
|
private val myProject: Project,
|
||||||
private val states: List<CoroutineState>
|
private val infoData: List<CoroutineInfoData>
|
||||||
) : ExporterToTextFile {
|
) : ExporterToTextFile {
|
||||||
|
|
||||||
override fun getReportText() = buildString {
|
override fun getReportText() = buildString {
|
||||||
for (state in states)
|
for (state in infoData)
|
||||||
append(state.stringStackTrace).append("\n\n")
|
append(state.stringStackTrace).append("\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDefaultFilePath() = (myProject.basePath ?: "") + File.separator + defaultReportFileName
|
override fun getDefaultFilePath() = (myProject.basePath ?: "") + File.separator + defaultReportFileName
|
||||||
|
|
||||||
override fun canExport() = states.isNotEmpty()
|
override fun canExport() = infoData.isNotEmpty()
|
||||||
|
|
||||||
private val defaultReportFileName = "coroutines_report.txt"
|
private val defaultReportFileName = "coroutines_report.txt"
|
||||||
}
|
}
|
||||||
+68
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+94
@@ -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<CoroutineInfoData> = mutableListOf(), var state: CacheState = CacheState.INIT
|
||||||
|
) {
|
||||||
|
fun ok(infoList: List<CoroutineInfoData>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
+43
@@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+263
@@ -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<XValueContainer>(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<CoroutineGroupContainer>(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<CreationCoroutineStackFrameItem>()
|
||||||
|
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<CreationCoroutineStackFrameItem>) : 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
|
||||||
|
}
|
||||||
+9
@@ -222,6 +222,15 @@ class KotlinStackFrame(frame: StackFrameProxyImpl) : JavaStackFrame(StackFrameDe
|
|||||||
@Suppress("ConvertToStringTemplate")
|
@Suppress("ConvertToStringTemplate")
|
||||||
return name().startsWith(AsmUtil.LABELED_THIS_PARAMETER)
|
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 {
|
interface ThisLocalVariable {
|
||||||
|
|||||||
+3
-3
@@ -10,7 +10,7 @@ import com.intellij.debugger.engine.JavaValue
|
|||||||
import com.intellij.debugger.memory.utils.StackFrameItem
|
import com.intellij.debugger.memory.utils.StackFrameItem
|
||||||
import com.intellij.execution.process.ProcessOutputTypes
|
import com.intellij.execution.process.ProcessOutputTypes
|
||||||
import com.intellij.openapi.extensions.Extensions
|
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.idea.debugger.test.preference.DebuggerPreferences
|
||||||
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
|
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
|
||||||
import org.jetbrains.kotlin.utils.getSafe
|
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)
|
val area = Extensions.getArea(null)
|
||||||
if (!area.hasExtensionPoint(ASYNC_STACKTRACE_EP_NAME)) {
|
if (!area.hasExtensionPoint(ASYNC_STACKTRACE_EP_NAME)) {
|
||||||
System.err.println("$ASYNC_STACKTRACE_EP_NAME extension point is not found (probably old IDE version)")
|
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<Any>(ASYNC_STACKTRACE_EP_NAME)
|
val extensionPoint = area.getExtensionPoint<Any>(ASYNC_STACKTRACE_EP_NAME)
|
||||||
val provider = extensionPoint.extensions.firstIsInstanceOrNull<KotlinCoroutinesAsyncStackTraceProvider>()
|
val provider = extensionPoint.extensions.firstIsInstanceOrNull<CoroutineAsyncStackTraceProvider>()
|
||||||
|
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
System.err.println("Kotlin coroutine async stack trace provider is not found")
|
System.err.println("Kotlin coroutine async stack trace provider is not found")
|
||||||
|
|||||||
+12
-18
@@ -5,59 +5,53 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.idea.debugger.test
|
package org.jetbrains.kotlin.idea.debugger.test
|
||||||
|
|
||||||
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
|
|
||||||
import com.intellij.execution.configurations.JavaParameters
|
import com.intellij.execution.configurations.JavaParameters
|
||||||
import com.intellij.execution.process.ProcessOutputTypes
|
import com.intellij.execution.process.ProcessOutputTypes
|
||||||
import com.intellij.jarRepository.JarRepositoryManager
|
import com.intellij.jarRepository.JarRepositoryManager
|
||||||
import com.intellij.jarRepository.RemoteRepositoryDescription
|
import com.intellij.jarRepository.RemoteRepositoryDescription
|
||||||
import org.jetbrains.idea.maven.aether.ArtifactKind
|
import org.jetbrains.idea.maven.aether.ArtifactKind
|
||||||
import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor
|
import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor
|
||||||
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineState
|
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
|
||||||
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutinesDebugProbesProxy
|
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.CoroutinesDebugProbesProxy
|
||||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
|
||||||
import org.jetbrains.kotlin.idea.debugger.test.preference.DebuggerPreferences
|
import org.jetbrains.kotlin.idea.debugger.test.preference.DebuggerPreferences
|
||||||
|
|
||||||
abstract class AbstractCoroutineDumpTest : KotlinDescriptorTestCaseWithStepping() {
|
abstract class AbstractCoroutineDumpTest : KotlinDescriptorTestCaseWithStepping() {
|
||||||
override fun doMultiFileTest(files: TestFiles, preferences: DebuggerPreferences) {
|
override fun doMultiFileTest(files: TestFiles, preferences: DebuggerPreferences) {
|
||||||
doOnBreakpoint {
|
doOnBreakpoint {
|
||||||
val evalContext = EvaluationContextImpl(this, frameProxy)
|
val infoCache = CoroutinesDebugProbesProxy(this).dumpCoroutines()
|
||||||
val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint)
|
|
||||||
val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
|
|
||||||
try {
|
try {
|
||||||
if (either.isRight)
|
if (infoCache.isOk())
|
||||||
try {
|
try {
|
||||||
val states = either.get()
|
val states = infoCache.cache
|
||||||
print(stringDump(states), ProcessOutputTypes.SYSTEM)
|
print(stringDump(states), ProcessOutputTypes.SYSTEM)
|
||||||
} catch (ignored: Throwable) {
|
} catch (ignored: Throwable) {
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw AssertionError("Dump failed", either.left)
|
throw AssertionError("Dump failed")
|
||||||
} finally {
|
} finally {
|
||||||
resume(this)
|
resume(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
doOnBreakpoint {
|
doOnBreakpoint {
|
||||||
val evalContext = EvaluationContextImpl(this, frameProxy)
|
val infoCache = CoroutinesDebugProbesProxy(this).dumpCoroutines()
|
||||||
val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint)
|
|
||||||
val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
|
|
||||||
try {
|
try {
|
||||||
if (either.isRight)
|
if (infoCache.isOk())
|
||||||
try {
|
try {
|
||||||
val states = either.get()
|
val states = infoCache.cache
|
||||||
print(stringDump(states), ProcessOutputTypes.SYSTEM)
|
print(stringDump(states), ProcessOutputTypes.SYSTEM)
|
||||||
} catch (ignored: Throwable) {
|
} catch (ignored: Throwable) {
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw AssertionError("Dump failed", either.left)
|
throw AssertionError("Dump failed")
|
||||||
} finally {
|
} finally {
|
||||||
resume(this)
|
resume(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stringDump(states: List<CoroutineState>) = buildString {
|
private fun stringDump(infoData: List<CoroutineInfoData>) = buildString {
|
||||||
states.forEach {
|
infoData.forEach {
|
||||||
appendln("\"${it.name}\", state: ${it.state}")
|
appendln("\"${it.name}\", state: ${it.state}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+62
@@ -18,6 +18,7 @@ import com.intellij.debugger.jdi.VirtualMachineProxyImpl
|
|||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.sun.jdi.*
|
import com.sun.jdi.*
|
||||||
import com.sun.jdi.request.EventRequest
|
import com.sun.jdi.request.EventRequest
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.hopelessAware
|
||||||
import org.jetbrains.org.objectweb.asm.Type
|
import org.jetbrains.org.objectweb.asm.Type
|
||||||
|
|
||||||
class ExecutionContext(val evaluationContext: EvaluationContextImpl, val frameProxy: StackFrameProxyImpl) {
|
class ExecutionContext(val evaluationContext: EvaluationContextImpl, val frameProxy: StackFrameProxyImpl) {
|
||||||
@@ -100,4 +101,65 @@ class ExecutionContext(val evaluationContext: EvaluationContextImpl, val framePr
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
DebuggerUtilsEx.keep(reference, evaluationContext)
|
DebuggerUtilsEx.keep(reference, evaluationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<group id="Kotlin.XDebugger.Actions">
|
<group id="Kotlin.XDebugger.Actions">
|
||||||
<!-- TODO(design)-->
|
<!-- TODO(design)-->
|
||||||
<action id="Kotlin.XDebugger.CoroutinesDump"
|
<action id="Kotlin.XDebugger.CoroutinesDump"
|
||||||
class="org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineDumpAction"
|
class="org.jetbrains.kotlin.idea.debugger.coroutines.command.CoroutineDumpAction"
|
||||||
text="Get Coroutines Dump"
|
text="Get Coroutines Dump"
|
||||||
icon="/org/jetbrains/kotlin/idea/icons/kotlin.png"/>
|
icon="/org/jetbrains/kotlin/idea/icons/kotlin.png"/>
|
||||||
</group>
|
</group>
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
|
|
||||||
<applicationService serviceImplementation="org.jetbrains.kotlin.idea.debugger.ToggleKotlinVariablesState"/>
|
<applicationService serviceImplementation="org.jetbrains.kotlin.idea.debugger.ToggleKotlinVariablesState"/>
|
||||||
|
|
||||||
<debugger.asyncStackTraceProvider implementation="org.jetbrains.kotlin.idea.debugger.KotlinCoroutinesAsyncStackTraceProvider"/>
|
<debugger.asyncStackTraceProvider implementation="org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineAsyncStackTraceProvider"/>
|
||||||
<debugger.jvmSmartStepIntoHandler implementation="org.jetbrains.kotlin.idea.debugger.stepping.smartStepInto.KotlinSmartStepIntoHandler"/>
|
<debugger.jvmSmartStepIntoHandler implementation="org.jetbrains.kotlin.idea.debugger.stepping.smartStepInto.KotlinSmartStepIntoHandler"/>
|
||||||
<debugger.positionManagerFactory implementation="org.jetbrains.kotlin.idea.debugger.KotlinPositionManagerFactory"/>
|
<debugger.positionManagerFactory implementation="org.jetbrains.kotlin.idea.debugger.KotlinPositionManagerFactory"/>
|
||||||
<debugger.codeFragmentFactory implementation="org.jetbrains.kotlin.idea.debugger.evaluate.KotlinCodeFragmentFactory"/>
|
<debugger.codeFragmentFactory implementation="org.jetbrains.kotlin.idea.debugger.evaluate.KotlinCodeFragmentFactory"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user