[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.initialization.label=Field &initialization
|
||||
|
||||
# Coroutine Debugger
|
||||
debugger.session.tab.coroutine.title=Coroutines
|
||||
debugger.session.tab.coroutine.message.failure=Failed to retrieve coroutine status
|
||||
debugger.session.tab.coroutine.message.resume=Application resumed
|
||||
debugger.session.tab.coroutine.message.error=Coroutine dump failed, see log
|
||||
debugger.session.tab.xcoroutine.title=XCoroutines
|
||||
|
||||
# Android Lint
|
||||
android.klint.inspections.group.name=Android Lint for Kotlin
|
||||
|
||||
|
||||
+2
-1
@@ -28,7 +28,7 @@ class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtensi
|
||||
val lines = arrayOf(
|
||||
"gradle.taskGraph.beforeTask { Task task ->",
|
||||
" if (task instanceof Test) {",
|
||||
" def kotlinxCoroutinesDebugJar = task.classpath.find { it.name.contains(\"kotlinx-coroutines-debug\") }",
|
||||
" def kotlinxCoroutinesDebugJar = task.classpath.find { it.name.startsWith(\"kotlinx-coroutines-debug\") }",
|
||||
" if (kotlinxCoroutinesDebugJar)",
|
||||
" task.jvmArgs (\"-javaagent:\${kotlinxCoroutinesDebugJar?.absolutePath}\", \"-ea\")",
|
||||
" }",
|
||||
@@ -38,5 +38,6 @@ class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtensi
|
||||
initScriptConsumer.consume(script)
|
||||
}
|
||||
|
||||
// supposed to be the same as [CoroutineProjectConnectionListener.kt].coroutineDebuggerEnabled
|
||||
private fun coroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines")
|
||||
}
|
||||
-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()
|
||||
}
|
||||
}
|
||||
+5
-14
@@ -9,31 +9,22 @@ import com.intellij.execution.configurations.DebuggingRunnerData
|
||||
import com.intellij.execution.configurations.JavaParameters
|
||||
import com.intellij.execution.configurations.RunConfigurationBase
|
||||
import com.intellij.execution.configurations.RunnerSettings
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
|
||||
/**
|
||||
* Installs coroutines debug agent and coroutines tab if `kotlinx.coroutines.debug` dependency is found
|
||||
* Installs coroutines debug agent and coroutines tab if `kotlinx-coroutines-debug` dependency is found
|
||||
*/
|
||||
@Suppress("IncompatibleAPI")
|
||||
class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() {
|
||||
private val log = Logger.getInstance(this::class.java)
|
||||
|
||||
override fun isApplicableFor(configuration: RunConfigurationBase<*>) = isCoroutineDebuggerEnabled()
|
||||
override fun isApplicableFor(configuration: RunConfigurationBase<*>) = coroutineDebuggerEnabled()
|
||||
|
||||
override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
|
||||
configuration: T,
|
||||
params: JavaParameters?,
|
||||
runnerSettings: RunnerSettings?
|
||||
) {
|
||||
if (!isCoroutineDebuggerEnabled()) return
|
||||
if (runnerSettings is DebuggingRunnerData) {
|
||||
try {
|
||||
val kotlinxCoroutinesClassPathLib = params?.classPath?.pathList?.first { it.contains("kotlinx-coroutines-debug") }
|
||||
initializeCoroutineAgent(params!!, kotlinxCoroutinesClassPathLib)
|
||||
registerProjectCoroutineListener(configuration)
|
||||
} catch (e: NoSuchElementException) {
|
||||
log.warn("'kotlinx-coroutines-debug' not found in classpath. Coroutine debugger disabled.")
|
||||
}
|
||||
if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) {
|
||||
configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-14
@@ -9,31 +9,22 @@ import com.intellij.execution.configurations.DebuggingRunnerData
|
||||
import com.intellij.execution.configurations.JavaParameters
|
||||
import com.intellij.execution.configurations.RunConfigurationBase
|
||||
import com.intellij.execution.configurations.RunnerSettings
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
|
||||
/**
|
||||
* Installs coroutines debug agent and coroutines tab if `kotlinx.coroutines.debug` dependency is found
|
||||
* Installs coroutines debug agent and coroutines tab if `kotlinx-coroutines-debug` dependency is found
|
||||
*/
|
||||
@Suppress("IncompatibleAPI")
|
||||
class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() {
|
||||
private val log = Logger.getInstance(this::class.java)
|
||||
|
||||
override fun isApplicableFor(configuration: RunConfigurationBase<*>) = isCoroutineDebuggerEnabled()
|
||||
override fun isApplicableFor(configuration: RunConfigurationBase<*>) = coroutineDebuggerEnabled()
|
||||
|
||||
override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
|
||||
configuration: T,
|
||||
params: JavaParameters,
|
||||
runnerSettings: RunnerSettings?
|
||||
) {
|
||||
if (!isCoroutineDebuggerEnabled()) return
|
||||
if (runnerSettings is DebuggingRunnerData) {
|
||||
try {
|
||||
val kotlinxCoroutinesClassPathLib = params?.classPath?.pathList?.first { it.contains("kotlinx-coroutines-debug") }
|
||||
initializeCoroutineAgent(params!!, kotlinxCoroutinesClassPathLib)
|
||||
registerProjectCoroutineListener(configuration)
|
||||
} catch (e: NoSuchElementException) {
|
||||
log.warn("'kotlinx-coroutines-debug' not found in classpath. Coroutine debugger disabled.")
|
||||
}
|
||||
if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) {
|
||||
configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-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.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||
package org.jetbrains.kotlin.idea.debugger.coroutines.command
|
||||
|
||||
import com.intellij.debugger.DebuggerManagerEx
|
||||
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
|
||||
@@ -21,11 +21,14 @@ import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.MessageType
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import com.intellij.util.text.DateFormatUtil
|
||||
import com.intellij.xdebugger.impl.XDebuggerManagerImpl
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||
import org.jetbrains.kotlin.idea.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutines.view.CoroutineDumpPanel
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutines.coroutineDebuggerEnabled
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.CoroutinesDebugProbesProxy
|
||||
|
||||
@Suppress("ComponentNotRegistered")
|
||||
class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
|
||||
@@ -39,28 +42,23 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
|
||||
val process = context.debugProcess ?: return
|
||||
process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
|
||||
override fun contextAction() {
|
||||
val evalContext = context.createEvaluationContext()
|
||||
val frameProxy = evalContext?.frameProxy ?: return
|
||||
val execContext = ExecutionContext(evalContext, frameProxy)
|
||||
val states = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
|
||||
if (states.isLeft) {
|
||||
logger.warn(states.left)
|
||||
XDebuggerManagerImpl.NOTIFICATION_GROUP
|
||||
.createNotification(
|
||||
"Coroutine dump failed. See log",
|
||||
MessageType.WARNING
|
||||
).notify(project)
|
||||
return
|
||||
val states = CoroutinesDebugProbesProxy(context.suspendContext!!).dumpCoroutines() ?: return
|
||||
if (states.isOk()) {
|
||||
XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(
|
||||
KotlinBundle.message("debugger.session.tab.coroutine.message.error"),
|
||||
MessageType.ERROR
|
||||
).notify(project)
|
||||
} else {
|
||||
val f = fun() {
|
||||
addCoroutineDump(
|
||||
project,
|
||||
states.cache,
|
||||
session.xDebugSession?.ui ?: return,
|
||||
session.searchScope
|
||||
)
|
||||
}
|
||||
ApplicationManager.getApplication().invokeLater(f, ModalityState.NON_MODAL)
|
||||
}
|
||||
val f = fun() {
|
||||
addCoroutineDump(
|
||||
project,
|
||||
states.get(),
|
||||
session.xDebugSession?.ui ?: return,
|
||||
session.searchScope
|
||||
)
|
||||
}
|
||||
ApplicationManager.getApplication().invokeLater(f, ModalityState.NON_MODAL)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -69,7 +67,7 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
|
||||
/**
|
||||
* Analog of [DebuggerUtilsEx.addThreadDump].
|
||||
*/
|
||||
fun addCoroutineDump(project: Project, coroutines: List<CoroutineState>, ui: RunnerLayoutUi, searchScope: GlobalSearchScope) {
|
||||
fun addCoroutineDump(project: Project, coroutines: List<CoroutineInfoData>, ui: RunnerLayoutUi, searchScope: GlobalSearchScope) {
|
||||
val consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project)
|
||||
consoleBuilder.filters(ExceptionFilters.getFilters(searchScope))
|
||||
val consoleView = consoleBuilder.console
|
||||
@@ -102,7 +100,7 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
|
||||
return
|
||||
}
|
||||
val debuggerSession = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession
|
||||
presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached && isCoroutineDebuggerEnabled()
|
||||
presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached && coroutineDebuggerEnabled()
|
||||
presentation.isVisible = presentation.isEnabled
|
||||
}
|
||||
}
|
||||
+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.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||
package org.jetbrains.kotlin.idea.debugger.coroutines.data
|
||||
|
||||
import com.intellij.debugger.engine.JavaStackFrame
|
||||
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
|
||||
+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.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||
package org.jetbrains.kotlin.idea.debugger.coroutines.proxy
|
||||
|
||||
import com.sun.jdi.*
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||
import org.jetbrains.kotlin.idea.debugger.isSubtype
|
||||
|
||||
/**
|
||||
* Represents state of a coroutine.
|
||||
* @see `kotlinx.coroutines.debug.CoroutineInfo`
|
||||
*/
|
||||
class CoroutineState(
|
||||
val name: String,
|
||||
val state: State,
|
||||
val thread: ThreadReference? = null,
|
||||
val stackTrace: List<StackTraceElement>,
|
||||
val frame: ObjectReference?
|
||||
) {
|
||||
val isSuspended: Boolean = state == State.SUSPENDED
|
||||
val isEmptyStackTrace: Boolean by lazy { stackTrace.isEmpty() }
|
||||
val stringStackTrace: String by lazy {
|
||||
buildString {
|
||||
appendln("\"$name\", state: $state")
|
||||
stackTrace.forEach {
|
||||
appendln("\t$it")
|
||||
}
|
||||
}
|
||||
class LookupContinuation(val context: ExecutionContext, val frame: StackTraceElement) {
|
||||
|
||||
private fun suspendOrInvokeSuspend(method: Method): Boolean =
|
||||
"Lkotlin/coroutines/Continuation;)" in method.signature() ||
|
||||
(method.name() == "invokeSuspend" && method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;") // suspend fun or invokeSuspend
|
||||
|
||||
private fun findMethod() : Method {
|
||||
val clazz = context.findClass(frame.className) as ClassType
|
||||
val method = clazz.methodsByName(frame.methodName).last {
|
||||
val loc = it.location().lineNumber()
|
||||
loc < 0 && frame.lineNumber < 0 || loc > 0 && loc <= frame.lineNumber
|
||||
} // pick correct method if an overloaded one is given
|
||||
return method
|
||||
}
|
||||
|
||||
fun isApplicable(): Boolean {
|
||||
val method = findMethod()
|
||||
return suspendOrInvokeSuspend(method)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl)
|
||||
* @return null if given ObjectReference is not a BaseContinuationImpl instance or completion is null
|
||||
*/
|
||||
private fun getNextFrame(continuation: ObjectReference, context: ExecutionContext): ObjectReference? {
|
||||
val type = continuation.type() as ClassType
|
||||
if (!type.isSubtype("kotlin.coroutines.jvm.internal.BaseContinuationImpl")) return null
|
||||
val next = type.concreteMethodByName("getCompletion", "()Lkotlin/coroutines/Continuation;")
|
||||
return context.invokeMethod(continuation, next, emptyList()) as? ObjectReference
|
||||
}
|
||||
|
||||
/**
|
||||
* Find continuation for the [stackTraceElement]
|
||||
* Find continuation for the [frame]
|
||||
* Gets current CoroutineInfo.lastObservedFrame and finds next frames in it until null or needed stackTraceElement is found
|
||||
* @return null if matching continuation is not found or is not BaseContinuationImpl
|
||||
*/
|
||||
fun getContinuation(stackTraceElement: StackTraceElement, context: ExecutionContext): ObjectReference? {
|
||||
var continuation = frame ?: return null
|
||||
fun findContinuation(infoData: CoroutineInfoData): ObjectReference? {
|
||||
if (!isApplicable())
|
||||
return null
|
||||
|
||||
var continuation = infoData.frame ?: return null
|
||||
val baseType = "kotlin.coroutines.jvm.internal.BaseContinuationImpl"
|
||||
val getTrace = (continuation.type() as ClassType).concreteMethodByName(
|
||||
"getStackTraceElement",
|
||||
@@ -71,7 +62,7 @@ class CoroutineState(
|
||||
}
|
||||
|
||||
while (continuation.type().isSubtype(baseType)
|
||||
&& (stackTraceElement.className != className() || stackTraceElement.lineNumber != lineNumber())
|
||||
&& (frame.className != className() || frame.lineNumber != lineNumber())
|
||||
) {
|
||||
// while continuation is BaseContinuationImpl and it's frame equals to the current
|
||||
continuation = getNextFrame(continuation, context) ?: return null
|
||||
@@ -79,9 +70,15 @@ class CoroutineState(
|
||||
return if (continuation.type().isSubtype(baseType)) continuation else null
|
||||
}
|
||||
|
||||
enum class State {
|
||||
RUNNING,
|
||||
SUSPENDED,
|
||||
CREATED
|
||||
|
||||
/**
|
||||
* Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl)
|
||||
* @return null if given ObjectReference is not a BaseContinuationImpl instance or completion is null
|
||||
*/
|
||||
private fun getNextFrame(continuation: ObjectReference, context: ExecutionContext): ObjectReference? {
|
||||
val type = continuation.type() as ClassType
|
||||
if (!type.isSubtype("kotlin.coroutines.jvm.internal.BaseContinuationImpl")) return null
|
||||
val next = type.concreteMethodByName("getCompletion", "()Lkotlin/coroutines/Continuation;")
|
||||
return context.invokeMethod(continuation, next, emptyList()) as? ObjectReference
|
||||
}
|
||||
}
|
||||
+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.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||
package org.jetbrains.kotlin.idea.debugger.coroutines.view
|
||||
|
||||
import com.intellij.codeInsight.highlighting.HighlightManager
|
||||
import com.intellij.execution.ui.ConsoleView
|
||||
@@ -32,6 +32,7 @@ import com.intellij.ui.components.JBList
|
||||
import com.intellij.unscramble.AnalyzeStacktraceUtil
|
||||
import com.intellij.util.PlatformIcons
|
||||
import com.intellij.util.ui.EmptyIcon
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
import java.awt.datatransfer.StringSelection
|
||||
@@ -42,10 +43,10 @@ import javax.swing.event.DocumentEvent
|
||||
/**
|
||||
* Panel with dump of coroutines
|
||||
*/
|
||||
class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActions: DefaultActionGroup, val dump: List<CoroutineState>) :
|
||||
class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActions: DefaultActionGroup, val dump: List<CoroutineInfoData>) :
|
||||
JPanel(BorderLayout()), DataProvider {
|
||||
private var exporterToTextFile: ExporterToTextFile
|
||||
private var mergedDump = ArrayList<CoroutineState>()
|
||||
private var mergedDump = ArrayList<CoroutineInfoData>()
|
||||
val filterField = SearchTextField()
|
||||
val filterPanel = JPanel(BorderLayout())
|
||||
private val coroutinesList = JBList(DefaultListModel<Any>())
|
||||
@@ -71,7 +72,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
|
||||
addListSelectionListener {
|
||||
val index = selectedIndex
|
||||
if (index >= 0) {
|
||||
val selection = model.getElementAt(index) as CoroutineState
|
||||
val selection = model.getElementAt(index) as CoroutineInfoData
|
||||
AnalyzeStacktraceUtil.printStacktrace(consoleView, selection.stringStackTrace)
|
||||
} else {
|
||||
AnalyzeStacktraceUtil.printStacktrace(consoleView, "")
|
||||
@@ -80,7 +81,8 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
|
||||
}
|
||||
}
|
||||
|
||||
exporterToTextFile = MyToFileExporter(project, dump)
|
||||
exporterToTextFile =
|
||||
MyToFileExporter(project, dump)
|
||||
|
||||
val filterAction = FilterAction().apply {
|
||||
registerCustomShortcutSet(
|
||||
@@ -90,7 +92,9 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
|
||||
}
|
||||
toolbarActions.apply {
|
||||
add(filterAction)
|
||||
add(CopyToClipboardAction(dump, project))
|
||||
add(
|
||||
CopyToClipboardAction(dump, project)
|
||||
)
|
||||
add(ActionManager.getInstance().getAction(IdeActions.ACTION_EXPORT_TO_TEXT_FILE))
|
||||
add(MergeStackTracesAction())
|
||||
}
|
||||
@@ -174,18 +178,18 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
|
||||
|
||||
override fun getData(dataId: String): Any? = if (PlatformDataKeys.EXPORTER_TO_TEXT_FILE.`is`(dataId)) exporterToTextFile else null
|
||||
|
||||
private fun getCoroutineStateIcon(state: CoroutineState): Icon {
|
||||
return when (state.state) {
|
||||
CoroutineState.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign)
|
||||
CoroutineState.State.SUSPENDED -> AllIcons.Actions.Pause
|
||||
private fun getCoroutineStateIcon(infoData: CoroutineInfoData): Icon {
|
||||
return when (infoData.state) {
|
||||
CoroutineInfoData.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign)
|
||||
CoroutineInfoData.State.SUSPENDED -> AllIcons.Actions.Pause
|
||||
else -> EmptyIcon.create(6)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAttributes(state: CoroutineState): SimpleTextAttributes {
|
||||
private fun getAttributes(infoData: CoroutineInfoData): SimpleTextAttributes {
|
||||
return when {
|
||||
state.isSuspended -> SimpleTextAttributes.GRAY_ATTRIBUTES
|
||||
state.isEmptyStackTrace -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter())
|
||||
infoData.isSuspended() -> SimpleTextAttributes.GRAY_ATTRIBUTES
|
||||
infoData.isEmptyStackTrace() -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter())
|
||||
else -> SimpleTextAttributes.REGULAR_ATTRIBUTES
|
||||
|
||||
}
|
||||
@@ -194,7 +198,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
|
||||
private inner class CoroutineListCellRenderer : ColoredListCellRenderer<Any>() {
|
||||
|
||||
override fun customizeCellRenderer(list: JList<*>, value: Any, index: Int, selected: Boolean, hasFocus: Boolean) {
|
||||
val state = value as CoroutineState
|
||||
val state = value as CoroutineInfoData
|
||||
icon = getCoroutineStateIcon(state)
|
||||
val attrs = getAttributes(state)
|
||||
append(state.name + " (", attrs)
|
||||
@@ -246,7 +250,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
|
||||
}
|
||||
}
|
||||
|
||||
private class CopyToClipboardAction(private val myCoroutinesDump: List<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) {
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
@@ -268,17 +272,17 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi
|
||||
|
||||
private class MyToFileExporter(
|
||||
private val myProject: Project,
|
||||
private val states: List<CoroutineState>
|
||||
private val infoData: List<CoroutineInfoData>
|
||||
) : ExporterToTextFile {
|
||||
|
||||
override fun getReportText() = buildString {
|
||||
for (state in states)
|
||||
for (state in infoData)
|
||||
append(state.stringStackTrace).append("\n\n")
|
||||
}
|
||||
|
||||
override fun getDefaultFilePath() = (myProject.basePath ?: "") + File.separator + defaultReportFileName
|
||||
|
||||
override fun canExport() = states.isNotEmpty()
|
||||
override fun canExport() = infoData.isNotEmpty()
|
||||
|
||||
private val defaultReportFileName = "coroutines_report.txt"
|
||||
}
|
||||
+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")
|
||||
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 {
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ import com.intellij.debugger.engine.JavaValue
|
||||
import com.intellij.debugger.memory.utils.StackFrameItem
|
||||
import com.intellij.execution.process.ProcessOutputTypes
|
||||
import com.intellij.openapi.extensions.Extensions
|
||||
import org.jetbrains.kotlin.idea.debugger.KotlinCoroutinesAsyncStackTraceProvider
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineAsyncStackTraceProvider
|
||||
import org.jetbrains.kotlin.idea.debugger.test.preference.DebuggerPreferences
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
|
||||
import org.jetbrains.kotlin.utils.getSafe
|
||||
@@ -54,7 +54,7 @@ abstract class AbstractAsyncStackTraceTest : KotlinDescriptorTestCaseWithSteppin
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAsyncStackTraceProvider(): KotlinCoroutinesAsyncStackTraceProvider? {
|
||||
private fun getAsyncStackTraceProvider(): CoroutineAsyncStackTraceProvider? {
|
||||
val area = Extensions.getArea(null)
|
||||
if (!area.hasExtensionPoint(ASYNC_STACKTRACE_EP_NAME)) {
|
||||
System.err.println("$ASYNC_STACKTRACE_EP_NAME extension point is not found (probably old IDE version)")
|
||||
@@ -62,7 +62,7 @@ abstract class AbstractAsyncStackTraceTest : KotlinDescriptorTestCaseWithSteppin
|
||||
}
|
||||
|
||||
val extensionPoint = area.getExtensionPoint<Any>(ASYNC_STACKTRACE_EP_NAME)
|
||||
val provider = extensionPoint.extensions.firstIsInstanceOrNull<KotlinCoroutinesAsyncStackTraceProvider>()
|
||||
val provider = extensionPoint.extensions.firstIsInstanceOrNull<CoroutineAsyncStackTraceProvider>()
|
||||
|
||||
if (provider == null) {
|
||||
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
|
||||
|
||||
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
|
||||
import com.intellij.execution.configurations.JavaParameters
|
||||
import com.intellij.execution.process.ProcessOutputTypes
|
||||
import com.intellij.jarRepository.JarRepositoryManager
|
||||
import com.intellij.jarRepository.RemoteRepositoryDescription
|
||||
import org.jetbrains.idea.maven.aether.ArtifactKind
|
||||
import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineState
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutines.CoroutinesDebugProbesProxy
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutines.data.CoroutineInfoData
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutines.proxy.CoroutinesDebugProbesProxy
|
||||
import org.jetbrains.kotlin.idea.debugger.test.preference.DebuggerPreferences
|
||||
|
||||
abstract class AbstractCoroutineDumpTest : KotlinDescriptorTestCaseWithStepping() {
|
||||
override fun doMultiFileTest(files: TestFiles, preferences: DebuggerPreferences) {
|
||||
doOnBreakpoint {
|
||||
val evalContext = EvaluationContextImpl(this, frameProxy)
|
||||
val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint)
|
||||
val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
|
||||
val infoCache = CoroutinesDebugProbesProxy(this).dumpCoroutines()
|
||||
try {
|
||||
if (either.isRight)
|
||||
if (infoCache.isOk())
|
||||
try {
|
||||
val states = either.get()
|
||||
val states = infoCache.cache
|
||||
print(stringDump(states), ProcessOutputTypes.SYSTEM)
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
else
|
||||
throw AssertionError("Dump failed", either.left)
|
||||
throw AssertionError("Dump failed")
|
||||
} finally {
|
||||
resume(this)
|
||||
}
|
||||
}
|
||||
|
||||
doOnBreakpoint {
|
||||
val evalContext = EvaluationContextImpl(this, frameProxy)
|
||||
val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint)
|
||||
val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
|
||||
val infoCache = CoroutinesDebugProbesProxy(this).dumpCoroutines()
|
||||
try {
|
||||
if (either.isRight)
|
||||
if (infoCache.isOk())
|
||||
try {
|
||||
val states = either.get()
|
||||
val states = infoCache.cache
|
||||
print(stringDump(states), ProcessOutputTypes.SYSTEM)
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
else
|
||||
throw AssertionError("Dump failed", either.left)
|
||||
throw AssertionError("Dump failed")
|
||||
} finally {
|
||||
resume(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stringDump(states: List<CoroutineState>) = buildString {
|
||||
states.forEach {
|
||||
private fun stringDump(infoData: List<CoroutineInfoData>) = buildString {
|
||||
infoData.forEach {
|
||||
appendln("\"${it.name}\", state: ${it.state}")
|
||||
}
|
||||
}
|
||||
|
||||
+63
-1
@@ -18,6 +18,7 @@ import com.intellij.debugger.jdi.VirtualMachineProxyImpl
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.sun.jdi.*
|
||||
import com.sun.jdi.request.EventRequest
|
||||
import org.jetbrains.kotlin.idea.debugger.hopelessAware
|
||||
import org.jetbrains.org.objectweb.asm.Type
|
||||
|
||||
class ExecutionContext(val evaluationContext: EvaluationContextImpl, val frameProxy: StackFrameProxyImpl) {
|
||||
@@ -100,4 +101,65 @@ class ExecutionContext(val evaluationContext: EvaluationContextImpl, val framePr
|
||||
@Suppress("DEPRECATION")
|
||||
DebuggerUtilsEx.keep(reference, evaluationContext)
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
<!-- TODO(design)-->
|
||||
<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"
|
||||
icon="/org/jetbrains/kotlin/idea/icons/kotlin.png"/>
|
||||
</group>
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
<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.positionManagerFactory implementation="org.jetbrains.kotlin.idea.debugger.KotlinPositionManagerFactory"/>
|
||||
<debugger.codeFragmentFactory implementation="org.jetbrains.kotlin.idea.debugger.evaluate.KotlinCodeFragmentFactory"/>
|
||||
|
||||
Reference in New Issue
Block a user