coroutine debugger logic moved to jvm-debugger-coroutine module
This commit is contained in:
committed by
Vladimir Ilmov
parent
b1b0817336
commit
e570450c59
@@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
id("jps-compatible")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile(project(":idea:jvm-debugger:jvm-debugger-core"))
|
||||
|
||||
compileOnly(intellijDep())
|
||||
|
||||
Platform[192].orHigher {
|
||||
compileOnly(intellijPluginDep("java"))
|
||||
}
|
||||
|
||||
testCompile(project(":kotlin-test:kotlin-test-junit"))
|
||||
testCompile(commonDep("junit:junit"))
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
"main" { projectDefault() }
|
||||
"test" { none() }
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.coroutine
|
||||
|
||||
import com.intellij.debugger.engine.*
|
||||
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
|
||||
import com.intellij.debugger.jdi.StackFrameProxyImpl
|
||||
import com.intellij.xdebugger.frame.XSuspendContext
|
||||
import com.sun.jdi.*
|
||||
import org.jetbrains.kotlin.idea.debugger.*
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineAsyncStackFrameItem
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.AsyncStackTraceContext
|
||||
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 getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: XSuspendContext): List<CoroutineAsyncStackFrameItem>? {
|
||||
val stackFrameList = hopelessAware { getAsyncStackTraceSafe(stackFrame.stackFrameProxy, suspendContext) } ?: emptyList()
|
||||
return null
|
||||
}
|
||||
|
||||
fun getAsyncStackTraceSafe(frameProxy: StackFrameProxyImpl, suspendContext: XSuspendContext): 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 || !canRunEvaluation(suspendContext))
|
||||
return defaultResult
|
||||
|
||||
|
||||
val astContext = createAsyncStackTraceContext(frameProxy, suspendContext, method)
|
||||
return astContext.getAsyncStackTraceIfAny()
|
||||
}
|
||||
|
||||
private fun createAsyncStackTraceContext(
|
||||
frameProxy: StackFrameProxyImpl,
|
||||
suspendContext: XSuspendContext,
|
||||
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)
|
||||
}
|
||||
|
||||
fun canRunEvaluation(suspendContext: XSuspendContext) =
|
||||
(suspendContext as SuspendContextImpl).debugProcess.canRunEvaluation
|
||||
}
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.coroutine
|
||||
|
||||
import com.intellij.execution.RunConfigurationExtension
|
||||
import com.intellij.execution.configurations.DebuggingRunnerData
|
||||
import com.intellij.execution.configurations.JavaParameters
|
||||
import com.intellij.execution.configurations.RunConfigurationBase
|
||||
import com.intellij.execution.configurations.RunnerSettings
|
||||
|
||||
/**
|
||||
* Installs coroutines debug agent and coroutines tab if `kotlinx-coroutines-debug` dependency is found
|
||||
*/
|
||||
@Suppress("IncompatibleAPI")
|
||||
class CoroutineDebugConfigurationExtension : RunConfigurationExtension() {
|
||||
|
||||
override fun isApplicableFor(configuration: RunConfigurationBase<*>) = coroutineDebuggerEnabled()
|
||||
|
||||
override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
|
||||
configuration: T,
|
||||
params: JavaParameters?,
|
||||
runnerSettings: RunnerSettings?
|
||||
) {
|
||||
if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) {
|
||||
configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.coroutine
|
||||
|
||||
import com.intellij.execution.RunConfigurationExtension
|
||||
import com.intellij.execution.configurations.DebuggingRunnerData
|
||||
import com.intellij.execution.configurations.JavaParameters
|
||||
import com.intellij.execution.configurations.RunConfigurationBase
|
||||
import com.intellij.execution.configurations.RunnerSettings
|
||||
|
||||
/**
|
||||
* Installs coroutines debug agent and coroutines tab if `kotlinx-coroutines-debug` dependency is found
|
||||
*/
|
||||
@Suppress("IncompatibleAPI")
|
||||
class CoroutineDebugConfigurationExtension : RunConfigurationExtension() {
|
||||
|
||||
override fun isApplicableFor(configuration: RunConfigurationBase<*>) = coroutineDebuggerEnabled()
|
||||
|
||||
override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
|
||||
configuration: T,
|
||||
params: JavaParameters,
|
||||
runnerSettings: RunnerSettings?
|
||||
) {
|
||||
if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) {
|
||||
configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
+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.coroutine
|
||||
|
||||
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.coroutine
|
||||
|
||||
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.coroutine.util.*
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.view.CoroutinesPanel
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.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")
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.coroutine.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
|
||||
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.coroutine.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.coroutine.data.CreationFramesDescriptor
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineCreatedStackFrameDescriptor
|
||||
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
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()
|
||||
for(it in descriptor.frames) {
|
||||
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.coroutine.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.coroutine.CoroutineAsyncStackTraceProvider
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.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.activeThread == null) {
|
||||
myChildren.add(myNodeManager.createMessageNode("Frames are not available"))
|
||||
return true
|
||||
}
|
||||
val proxy = ThreadReferenceProxyImpl(
|
||||
debugProcess.virtualMachineProxy,
|
||||
descriptor.infoData.activeThread
|
||||
)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
+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.coroutine.command
|
||||
|
||||
import com.intellij.debugger.DebuggerManagerEx
|
||||
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.openapi.project.Project
|
||||
import com.intellij.util.containers.ContainerUtil
|
||||
import com.intellij.xdebugger.XSourcePosition
|
||||
import com.intellij.xdebugger.frame.XExecutionStack
|
||||
import com.intellij.xdebugger.frame.XStackFrame
|
||||
import com.intellij.xdebugger.frame.XSuspendContext
|
||||
import com.sun.jdi.*
|
||||
import org.jetbrains.kotlin.idea.debugger.*
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineAsyncStackTraceProvider
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.SuspendStackFrameDescriptor
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.SyntheticStackFrame
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.AsyncStackTraceContext
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.util.EmptyStackFrameDescriptor
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||
|
||||
|
||||
class CoroutineBuilder(val suspendContext: XSuspendContext) {
|
||||
private val methodsTracker = MethodsTracker()
|
||||
private val coroutineStackFrameProvider = CoroutineAsyncStackTraceProvider()
|
||||
val debugProcess = (suspendContext as SuspendContextImpl).debugProcess
|
||||
val virtualMachineProxy = 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(coroutine: CoroutineInfoData): List<CoroutineStackFrameItem> {
|
||||
val coroutineStackFrameList = mutableListOf<CoroutineStackFrameItem>()
|
||||
val firstSuspendedStackFrameProxyImpl = firstSuspendedThreadFrame()
|
||||
val creationFrameSeparatorIndex = findCreationFrameIndex(coroutine.stackTrace)
|
||||
val positionManager = debugProcess.positionManager
|
||||
|
||||
if (coroutine.state == CoroutineInfoData.State.RUNNING && coroutine.activeThread is ThreadReference) {
|
||||
val threadReferenceProxyImpl = runningThreadProxy(coroutine.activeThread)
|
||||
val executionStack = JavaExecutionStack(threadReferenceProxyImpl, debugProcess, suspendedSameThread(coroutine.activeThread))
|
||||
|
||||
val frames = threadReferenceProxyImpl.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 { asyncFrame ->
|
||||
val xStackFrame = JavaStackFrame(StackFrameDescriptorImpl(previousFrame, methodsTracker), true)
|
||||
coroutineStackFrameList.add(AsyncCoroutineStackFrameItem(runningStackFrameProxy, asyncFrame, xStackFrame))
|
||||
}
|
||||
} else {
|
||||
val xStackFrame = executionStack.createStackFrame(runningStackFrameProxy)
|
||||
coroutineStackFrameList.add(RunningCoroutineStackFrameItem(runningStackFrameProxy, xStackFrame))
|
||||
}
|
||||
}
|
||||
} else if (coroutine.state == CoroutineInfoData.State.SUSPENDED || coroutine.activeThread == null) {
|
||||
// to get frames from CoroutineInfo anyway
|
||||
// the thread is paused on breakpoint - it has at least one frame
|
||||
val suspendedStackTrace = coroutine.stackTrace.take(creationFrameSeparatorIndex + 1)
|
||||
for (suspendedFrame in suspendedStackTrace) {
|
||||
val suspendedXStackFrame = stackFrame(positionManager, firstSuspendedStackFrameProxyImpl, suspendedFrame)
|
||||
coroutineStackFrameList.add(
|
||||
SuspendCoroutineStackFrameItem(firstSuspendedStackFrameProxyImpl, suspendedFrame, suspendedXStackFrame)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val executionStack = JavaExecutionStack(suspendedThreadProxy(), debugProcess, false)
|
||||
val xStackFrame = executionStack.createStackFrame(firstSuspendedStackFrameProxyImpl)
|
||||
|
||||
coroutine.stackTrace.subList(creationFrameSeparatorIndex + 1, coroutine.stackTrace.size).forEach {
|
||||
var location = createLocation(it)
|
||||
coroutineStackFrameList.add(CreationCoroutineStackFrameItem(firstSuspendedStackFrameProxyImpl, it, xStackFrame, location))
|
||||
}
|
||||
coroutine.stackFrameList.addAll(coroutineStackFrameList)
|
||||
return coroutineStackFrameList
|
||||
}
|
||||
|
||||
private fun suspendedSameThread(activeThread: ThreadReference) =
|
||||
activeThread == suspendedThreadProxy().threadReference
|
||||
|
||||
private fun createLocation(stackTraceElement: StackTraceElement): Location = 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(debugProcess, type, methodName, line)
|
||||
}
|
||||
|
||||
fun stackFrame(positionManager: CompoundPositionManager, runningStackFrameProxy: StackFrameProxyImpl, location: Location): XStackFrame {
|
||||
return positionManager.createStackFrame(runningStackFrameProxy, debugProcess, location)!!
|
||||
}
|
||||
|
||||
fun stackFrame(positionManager: CompoundPositionManager, runningStackFrameProxy: StackFrameProxyImpl, stackTraceElement: StackTraceElement) : XStackFrame {
|
||||
val location = createLocation(stackTraceElement)
|
||||
return positionManager.createStackFrame(runningStackFrameProxy, 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 coroutine thread proxy
|
||||
fun runningThreadProxy(threadReference: ThreadReference) =
|
||||
ThreadReferenceProxyImpl(debugProcess.virtualMachineProxy, threadReference)
|
||||
|
||||
// retrieves current suspended thread proxy
|
||||
private fun suspendedThreadProxy(): ThreadReferenceProxyImpl =
|
||||
(suspendContext as SuspendContextImpl).thread!! // @TODO hash replace !!
|
||||
|
||||
private fun findResumeMethodIndex(frames: List<StackFrameProxyImpl>): Int {
|
||||
for (i: Int in frames.lastIndex downTo 0)
|
||||
if (isResumeMethodFrame(frames[i])) {
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun isResumeMethodFrame(frame: StackFrameProxyImpl) =
|
||||
frame.safeLocation()?.safeMethod()?.name() == "resumeWith"
|
||||
|
||||
/**
|
||||
* Should be invoked on manager thread
|
||||
* This code was migrated from previous implementation, has to be refactored @TODO
|
||||
*/
|
||||
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,
|
||||
val stackTraceElement: StackTraceElement,
|
||||
stackFrame: XStackFrame,
|
||||
val location: Location
|
||||
) : CoroutineStackFrameItem(frame, stackFrame) {
|
||||
override fun location() = location
|
||||
fun emptyDescriptor() =
|
||||
EmptyStackFrameDescriptor(stackTraceElement, frame)
|
||||
}
|
||||
|
||||
class SuspendCoroutineStackFrameItem(
|
||||
frame: StackFrameProxyImpl,
|
||||
val stackTraceElement: StackTraceElement,
|
||||
stackFrame: XStackFrame
|
||||
) : CoroutineStackFrameItem(frame, stackFrame) {
|
||||
override fun location() = frame.location()
|
||||
}
|
||||
|
||||
class AsyncCoroutineStackFrameItem(
|
||||
frame: StackFrameProxyImpl,
|
||||
val frameItem: StackFrameItem,
|
||||
stackFrame: XStackFrame
|
||||
) : CoroutineStackFrameItem(frame, stackFrame) {
|
||||
override fun location() : Location = frame.location()
|
||||
}
|
||||
|
||||
class RunningCoroutineStackFrameItem(
|
||||
frame: StackFrameProxyImpl,
|
||||
stackFrame: XStackFrame
|
||||
) : CoroutineStackFrameItem(frame, stackFrame) {
|
||||
val location = frame.location() // it should be invoked in manager thread
|
||||
|
||||
override fun location() = location
|
||||
}
|
||||
|
||||
abstract class CoroutineStackFrameItem(val frame: StackFrameProxyImpl, val stackFrame: XStackFrame) {
|
||||
val log by logger
|
||||
|
||||
fun sourcePosition() : XSourcePosition? = stackFrame.sourcePosition
|
||||
|
||||
abstract fun location(): Location
|
||||
|
||||
fun uniqueId(): String {
|
||||
val location = location()
|
||||
try {
|
||||
return location.safeSourceName() + ":" + location.safeMethod().toString() + ":" +
|
||||
location.safeLineNumber() + ":" + location.safeSourceLineNumber()
|
||||
} catch (e: Exception) {
|
||||
log.error(e)
|
||||
return location.method().toString() + ":" + location.lineNumber()
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.coroutine.command
|
||||
|
||||
import com.intellij.debugger.DebuggerManagerEx
|
||||
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
|
||||
import com.intellij.debugger.impl.DebuggerUtilsEx
|
||||
import com.intellij.execution.filters.ExceptionFilters
|
||||
import com.intellij.execution.filters.TextConsoleBuilderFactory
|
||||
import com.intellij.execution.ui.RunnerLayoutUi
|
||||
import com.intellij.execution.ui.layout.impl.RunnerContentUi
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.MessageType
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import com.intellij.util.text.DateFormatUtil
|
||||
import com.intellij.xdebugger.impl.XDebuggerManagerImpl
|
||||
import org.jetbrains.kotlin.idea.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.view.CoroutineDumpPanel
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.coroutineDebuggerEnabled
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.CoroutineDebugProbesProxy
|
||||
|
||||
@Suppress("ComponentNotRegistered")
|
||||
class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||
val session = context.debuggerSession
|
||||
if (session != null && session.isAttached) {
|
||||
val process = context.debugProcess ?: return
|
||||
process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
|
||||
override fun contextAction() {
|
||||
val states = CoroutineDebugProbesProxy(context.suspendContext ?: return)
|
||||
.dumpCoroutines()
|
||||
if (states.isOk()) {
|
||||
val message = KotlinBundle.message("debugger.session.tab.coroutine.message.error")
|
||||
XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(message,MessageType.ERROR).notify(project)
|
||||
} else {
|
||||
val f = fun() {
|
||||
val ui = session.xDebugSession?.ui ?: return
|
||||
addCoroutineDump(project, states.cache, ui, session.searchScope)
|
||||
}
|
||||
ApplicationManager.getApplication().invokeLater(f, ModalityState.NON_MODAL)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analog of [DebuggerUtilsEx.addThreadDump].
|
||||
*/
|
||||
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
|
||||
val toolbarActions = DefaultActionGroup()
|
||||
consoleView.allowHeavyFilters()
|
||||
val panel = CoroutineDumpPanel(project, consoleView, toolbarActions, coroutines)
|
||||
|
||||
val id = "DumpKt " + DateFormatUtil.formatTimeWithSeconds(System.currentTimeMillis())
|
||||
val content = ui.createContent(id, panel, id, null, null).apply {
|
||||
putUserData(RunnerContentUi.LIGHTWEIGHT_CONTENT_MARKER, true)
|
||||
isCloseable = true
|
||||
description = "Coroutine Dump"
|
||||
}
|
||||
ui.addContent(content)
|
||||
ui.selectAndFocus(content, true, true)
|
||||
Disposer.register(content, consoleView)
|
||||
}
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
val presentation = e.presentation
|
||||
val project = e.project
|
||||
if (project == null) {
|
||||
presentation.isEnabled = false
|
||||
presentation.isVisible = false
|
||||
return
|
||||
}
|
||||
// cannot be called when no SuspendContext
|
||||
if (DebuggerManagerEx.getInstanceEx(project).context.suspendContext == null) {
|
||||
presentation.isEnabled = false
|
||||
return
|
||||
}
|
||||
val debuggerSession = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession
|
||||
presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached && coroutineDebuggerEnabled()
|
||||
presentation.isVisible = presentation.isEnabled
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.coroutine.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 org.jetbrains.kotlin.idea.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineDescriptorData
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.CoroutineDebugProbesProxy
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.util.ProjectNotification
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.view.CoroutinesDebuggerTree
|
||||
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
class RefreshCoroutinesTreeCommand(
|
||||
val context: DebuggerContextImpl,
|
||||
private val debuggerTree: CoroutinesDebuggerTree
|
||||
) : SuspendContextCommandImpl(context.suspendContext) {
|
||||
val notification = ProjectNotification(debuggerTree.project)
|
||||
|
||||
override fun contextAction() {
|
||||
val nodeManagerImpl = debuggerTree.nodeFactory
|
||||
val root = nodeManagerImpl.defaultNode
|
||||
val suspendContext: SuspendContextImpl? = suspendContext
|
||||
if (context.debuggerSession is DebuggerSession && suspendContext is SuspendContextImpl && !suspendContext.isResumed) {
|
||||
var infoCache = CoroutineDebugProbesProxy(suspendContext).dumpCoroutines()
|
||||
if (infoCache.isOk()) {
|
||||
val evaluationContext = evaluationContext(suspendContext)
|
||||
for (state in infoCache.cache) {
|
||||
val descriptor = createCoroutineDescriptorNode(nodeManagerImpl, state, evaluationContext)
|
||||
root.add(descriptor)
|
||||
}
|
||||
setRoot(root)
|
||||
} else {
|
||||
debuggerTree.showMessage(KotlinBundle.message("debugger.session.tab.coroutine.message.failure"))
|
||||
notification.error(KotlinBundle.message("debugger.session.tab.coroutine.message.error"))
|
||||
}
|
||||
} else
|
||||
debuggerTree.showMessage(KotlinBundle.message("debugger.session.tab.coroutine.message.resume"))
|
||||
}
|
||||
|
||||
private fun evaluationContext(suspendContext : SuspendContextImpl) =
|
||||
EvaluationContextImpl(suspendContext, suspendContext.frameProxy)
|
||||
|
||||
private fun createCoroutineDescriptorNode(
|
||||
nodeFactory: NodeManagerImpl,
|
||||
coroutineInfoData: CoroutineInfoData,
|
||||
evaluationContext: EvaluationContextImpl
|
||||
): DebuggerTreeNodeImpl {
|
||||
return nodeFactory.createNode(
|
||||
nodeFactory.getDescriptor(
|
||||
null,
|
||||
CoroutineDescriptorData(coroutineInfoData)
|
||||
),
|
||||
evaluationContext
|
||||
)
|
||||
}
|
||||
|
||||
private fun setRoot(root: DebuggerTreeNodeImpl) {
|
||||
DebuggerInvocationUtil.swingInvokeLater(debuggerTree.project) {
|
||||
debuggerTree.mutableModel.setRoot(root)
|
||||
debuggerTree.treeChanged()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 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.coroutine.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)
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.coroutine.data
|
||||
|
||||
import com.sun.jdi.*
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.command.CoroutineStackFrameItem
|
||||
|
||||
/**
|
||||
* Represents state of a coroutine.
|
||||
* @see `kotlinx.coroutines.debug.CoroutineInfo`
|
||||
*/
|
||||
data class CoroutineInfoData(
|
||||
val name: String,
|
||||
val state: State,
|
||||
|
||||
val stackTrace: List<StackTraceElement>,
|
||||
// links to jdi.* references
|
||||
val activeThread: 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
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.coroutine.data
|
||||
|
||||
import com.intellij.debugger.engine.JavaStackFrame
|
||||
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
|
||||
import com.intellij.xdebugger.XSourcePosition
|
||||
import com.intellij.xdebugger.frame.XCompositeNode
|
||||
import com.intellij.xdebugger.frame.XNamedValue
|
||||
import com.intellij.xdebugger.frame.XValueChildrenList
|
||||
|
||||
/**
|
||||
* Puts the frameProxy into JavaStackFrame just to instantiate. SyntheticStackFrame provides it's own data for variables view.
|
||||
*/
|
||||
class SyntheticStackFrame(
|
||||
descriptor: StackFrameDescriptorImpl,
|
||||
private val vars: List<XNamedValue>,
|
||||
private val position: XSourcePosition
|
||||
) :
|
||||
JavaStackFrame(descriptor, true) {
|
||||
|
||||
override fun computeChildren(node: XCompositeNode) {
|
||||
val list = XValueChildrenList()
|
||||
vars.forEach { list.add(it) }
|
||||
node.addChildren(list, true)
|
||||
}
|
||||
|
||||
override fun getSourcePosition(): XSourcePosition? {
|
||||
return position
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
val frame = other as? JavaStackFrame ?: return false
|
||||
|
||||
return descriptor.frameProxy == frame.descriptor.frameProxy
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return descriptor.frameProxy.hashCode()
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.coroutine.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
|
||||
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
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.activeThread
|
||||
val name = thread?.name()?.substringBefore(" @${infoData.name}") ?: ""
|
||||
val threadState = if (thread != null) DebuggerUtilsEx.getThreadStatusText(thread.status()) else ""
|
||||
val threadName = if (name.isNotEmpty()) " on thread \"$name\":$threadState" else ""
|
||||
return "${infoData.name}: ${infoData.state} $threadName"
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
class CreationFramesDescriptor(val frames: List<StackTraceElement>) :
|
||||
MessageDescriptor("Coroutine creation stack trace", INFORMATION) {
|
||||
|
||||
override fun isExpandable() = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptor for suspend functions
|
||||
*/
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
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(".", "")
|
||||
val packDisplay = if (pack.isNotEmpty()) "{$pack}" else ""
|
||||
"$methodName:$lineNumber, ${className.substringAfterLast(".")} $packDisplay"
|
||||
}
|
||||
}
|
||||
|
||||
override fun getName() : String? = frame.methodName
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* For the case when no data inside frame is available
|
||||
*/
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
class CoroutineCreatedStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) :
|
||||
CoroutineStackFrameDescriptor(proxy) {
|
||||
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
|
||||
return with(frame) {
|
||||
val pack = className.substringBeforeLast(".", "")
|
||||
val packDisplay = if (pack.isNotEmpty()) "{$pack}" else ""
|
||||
"$methodName:$lineNumber, ${className.substringAfterLast(".")} $packDisplay"
|
||||
}
|
||||
}
|
||||
|
||||
override fun getName() = null
|
||||
}
|
||||
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
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(".", "")
|
||||
val packDisplay = if (pack.isNotEmpty()) "{$pack}" else ""
|
||||
"${method()}:${line()}, ${path().substringAfterLast(".")} $packDisplay"
|
||||
}
|
||||
}
|
||||
|
||||
override fun getName() = frame.method()
|
||||
}
|
||||
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
open class CoroutineStackFrameDescriptor(proxy: StackFrameProxyImpl) : StackFrameDescriptorImpl(proxy, MethodsTracker()) {
|
||||
override fun isExpandable() = false
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.coroutine.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.coroutine.proxy.LookupContinuation
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
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
|
||||
}
|
||||
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
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)
|
||||
*/
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
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)
|
||||
}
|
||||
+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.coroutine.proxy
|
||||
|
||||
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.coroutine.util.logger
|
||||
import org.jetbrains.kotlin.idea.debugger.SUSPEND_LAMBDA_CLASSES
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.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) ?: 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)
|
||||
}
|
||||
}
|
||||
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* 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.coroutine.proxy
|
||||
|
||||
import com.intellij.debugger.engine.SuspendContextImpl
|
||||
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
|
||||
import com.intellij.debugger.jdi.StackFrameProxyImpl
|
||||
import com.intellij.xdebugger.frame.XSuspendContext
|
||||
import com.sun.jdi.*
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.command.CoroutineBuilder
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.view.CoroutineInfoCache
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||
|
||||
class CoroutineDebugProbesProxy(val suspendContext: XSuspendContext) {
|
||||
private val log by logger
|
||||
|
||||
private var executionContext: ExecutionContext = executionContext()
|
||||
// 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
|
||||
val stackTrace = getStackTrace(instance)
|
||||
return CoroutineInfoData(
|
||||
name,
|
||||
CoroutineInfoData.State.valueOf(state),
|
||||
stackTrace,
|
||||
thread,
|
||||
lastObservedFrameFieldRef
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
private fun executionContext() : ExecutionContext {
|
||||
val evaluationContextImpl = EvaluationContextImpl(suspendContext as SuspendContextImpl, suspendContext.frameProxy)
|
||||
return ExecutionContext(evaluationContextImpl, suspendContext.frameProxy as StackFrameProxyImpl)
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.coroutine.proxy
|
||||
|
||||
import com.sun.jdi.*
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||
import org.jetbrains.kotlin.idea.debugger.isSubtype
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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",
|
||||
"()Ljava/lang/StackTraceElement;"
|
||||
)
|
||||
val stackTraceType = context.findClass("java.lang.StackTraceElement") as ClassType
|
||||
val getClassName = stackTraceType.concreteMethodByName("getClassName", "()Ljava/lang/String;")
|
||||
val getLineNumber = stackTraceType.concreteMethodByName("getLineNumber", "()I")
|
||||
val className = {
|
||||
val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference
|
||||
if (trace != null)
|
||||
(context.invokeMethod(trace, getClassName, emptyList()) as StringReference).value()
|
||||
else null
|
||||
}
|
||||
val lineNumber = {
|
||||
val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference
|
||||
if (trace != null)
|
||||
(context.invokeMethod(trace, getLineNumber, emptyList()) as IntegerValue).value()
|
||||
else null
|
||||
}
|
||||
|
||||
while (continuation.type().isSubtype(baseType)
|
||||
&& (frame.className != className() || frame.lineNumber != lineNumber())
|
||||
) {
|
||||
// while continuation is BaseContinuationImpl and it's frame equals to the current
|
||||
continuation = getNextFrame(continuation, context) ?: return null
|
||||
}
|
||||
return if (continuation.type().isSubtype(baseType)) continuation else null
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.coroutine.proxy
|
||||
|
||||
import com.intellij.debugger.engine.DebugProcessImpl
|
||||
import com.intellij.debugger.engine.SuspendContextImpl
|
||||
import com.intellij.debugger.engine.events.DebuggerCommandImpl
|
||||
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
|
||||
import com.intellij.debugger.impl.PrioritizedTask
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.util.Computable
|
||||
import com.intellij.xdebugger.frame.XSuspendContext
|
||||
import java.awt.Component
|
||||
|
||||
class ManagerThreadExecutor(val debugProcess: DebugProcessImpl) {
|
||||
fun on(suspendContext: SuspendContextImpl, priority: PrioritizedTask.Priority = PrioritizedTask.Priority.NORMAL) =
|
||||
ManagerThreadExecutorInstance(suspendContext, priority)
|
||||
|
||||
fun on(suspendContext: XSuspendContext, priority: PrioritizedTask.Priority = PrioritizedTask.Priority.NORMAL) =
|
||||
ManagerThreadExecutorInstance(suspendContext as SuspendContextImpl, priority)
|
||||
|
||||
inner class ManagerThreadExecutorInstance(
|
||||
val suspendContext: SuspendContextImpl,
|
||||
val priority: PrioritizedTask.Priority = PrioritizedTask.Priority.NORMAL
|
||||
) {
|
||||
|
||||
fun schedule(f: (SuspendContextImpl) -> Unit) {
|
||||
val suspendContextCommand = object : SuspendContextCommandImpl(suspendContext) {
|
||||
override fun getPriority() = this@ManagerThreadExecutorInstance.priority
|
||||
|
||||
override fun contextAction(suspendContext: SuspendContextImpl) {
|
||||
f(suspendContext)
|
||||
}
|
||||
}
|
||||
debugProcess.managerThread.invoke(suspendContextCommand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ApplicationThreadExecutor {
|
||||
fun <T> readAction(f: () -> T): T {
|
||||
return ApplicationManager.getApplication().runReadAction(Computable(f))
|
||||
}
|
||||
|
||||
fun schedule(f: () -> Unit, component: Component) {
|
||||
return ApplicationManager.getApplication().invokeLater( { f() }, ModalityState.stateForComponent(component))
|
||||
}
|
||||
|
||||
fun schedule(f: () -> Unit) {
|
||||
return ApplicationManager.getApplication().invokeLater { f() }
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.coroutine.util
|
||||
|
||||
import com.intellij.debugger.jdi.StackFrameProxyImpl
|
||||
import com.intellij.debugger.ui.impl.watch.MethodsTracker
|
||||
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
|
||||
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.xdebugger.XDebuggerUtil
|
||||
import com.intellij.xdebugger.XSourcePosition
|
||||
import com.intellij.xdebugger.impl.XDebuggerManagerImpl
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ApplicationThreadExecutor
|
||||
|
||||
fun getPosition(stackTraceElement: StackTraceElement, project: Project): XSourcePosition? {
|
||||
val psiFacade = JavaPsiFacade.getInstance(project)
|
||||
|
||||
val psiClass = ApplicationThreadExecutor().readAction {
|
||||
@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)
|
||||
}
|
||||
|
||||
class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) :
|
||||
StackFrameDescriptorImpl(proxy, MethodsTracker())
|
||||
|
||||
class ProjectNotification(val project: Project) {
|
||||
fun error(message: String) =
|
||||
XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(message, MessageType.ERROR).notify(project)
|
||||
}
|
||||
+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.coroutine.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
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.coroutine.util;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* 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.coroutine.view
|
||||
|
||||
import com.intellij.codeInsight.highlighting.HighlightManager
|
||||
import com.intellij.execution.ui.ConsoleView
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.icons.AllIcons.Debugger.ThreadStates.Daemon_sign
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.ide.ExporterToTextFile
|
||||
import com.intellij.ide.ui.UISettings
|
||||
import com.intellij.notification.NotificationGroup
|
||||
import com.intellij.openapi.actionSystem.*
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.colors.EditorColors
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.openapi.editor.event.DocumentListener
|
||||
import com.intellij.openapi.ide.CopyPasteManager
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.MessageType
|
||||
import com.intellij.openapi.ui.Splitter
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.openapi.wm.IdeFocusManager
|
||||
import com.intellij.openapi.wm.ToolWindowId
|
||||
import com.intellij.ui.*
|
||||
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.coroutine.data.CoroutineInfoData
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Color
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.io.File
|
||||
import javax.swing.*
|
||||
import javax.swing.event.DocumentEvent
|
||||
|
||||
/**
|
||||
* Panel with dump of coroutines
|
||||
*/
|
||||
class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActions: DefaultActionGroup, val dump: List<CoroutineInfoData>) :
|
||||
JPanel(BorderLayout()), DataProvider {
|
||||
private var exporterToTextFile: ExporterToTextFile
|
||||
private var mergedDump = ArrayList<CoroutineInfoData>()
|
||||
val filterField = SearchTextField()
|
||||
val filterPanel = JPanel(BorderLayout())
|
||||
private val coroutinesList = JBList(DefaultListModel<Any>())
|
||||
|
||||
init {
|
||||
mergedDump.addAll(dump)
|
||||
|
||||
filterField.addDocumentListener(object : DocumentAdapter() {
|
||||
override fun textChanged(e: DocumentEvent) {
|
||||
updateCoroutinesList()
|
||||
}
|
||||
})
|
||||
|
||||
filterPanel.apply {
|
||||
add(JLabel("Filter:"), BorderLayout.WEST)
|
||||
add(filterField)
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
coroutinesList.apply {
|
||||
cellRenderer = CoroutineListCellRenderer()
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
addListSelectionListener {
|
||||
val index = selectedIndex
|
||||
if (index >= 0) {
|
||||
val selection = model.getElementAt(index) as CoroutineInfoData
|
||||
AnalyzeStacktraceUtil.printStacktrace(consoleView, selection.stringStackTrace)
|
||||
} else {
|
||||
AnalyzeStacktraceUtil.printStacktrace(consoleView, "")
|
||||
}
|
||||
repaint()
|
||||
}
|
||||
}
|
||||
|
||||
exporterToTextFile =
|
||||
MyToFileExporter(project, dump)
|
||||
|
||||
val filterAction = FilterAction().apply {
|
||||
registerCustomShortcutSet(
|
||||
ActionManager.getInstance().getAction(IdeActions.ACTION_FIND).shortcutSet,
|
||||
coroutinesList
|
||||
)
|
||||
}
|
||||
toolbarActions.apply {
|
||||
add(filterAction)
|
||||
add(
|
||||
CopyToClipboardAction(dump, project)
|
||||
)
|
||||
add(ActionManager.getInstance().getAction(IdeActions.ACTION_EXPORT_TO_TEXT_FILE))
|
||||
add(MergeStackTracesAction())
|
||||
}
|
||||
add(
|
||||
ActionManager.getInstance()
|
||||
.createActionToolbar("CoroutinesDump", toolbarActions, false).component,
|
||||
BorderLayout.WEST
|
||||
)
|
||||
|
||||
val leftPanel = JPanel(BorderLayout()).apply {
|
||||
add(filterPanel, BorderLayout.NORTH)
|
||||
add(ScrollPaneFactory.createScrollPane(coroutinesList, SideBorder.LEFT or SideBorder.RIGHT), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
val splitter = Splitter(false, 0.3f).apply {
|
||||
firstComponent = leftPanel
|
||||
secondComponent = consoleView.component
|
||||
}
|
||||
add(splitter, BorderLayout.CENTER)
|
||||
|
||||
ListSpeedSearch(coroutinesList).comparator = SpeedSearchComparator(false, true)
|
||||
|
||||
updateCoroutinesList()
|
||||
|
||||
val editor = CommonDataKeys.EDITOR.getData(
|
||||
DataManager.getInstance()
|
||||
.getDataContext(consoleView.preferredFocusableComponent)
|
||||
)
|
||||
editor?.document?.addDocumentListener(object : DocumentListener {
|
||||
override fun documentChanged(e: com.intellij.openapi.editor.event.DocumentEvent) {
|
||||
val filter = filterField.text
|
||||
if (StringUtil.isNotEmpty(filter)) {
|
||||
highlightOccurrences(filter, project, editor)
|
||||
}
|
||||
}
|
||||
}, consoleView)
|
||||
}
|
||||
|
||||
private fun updateCoroutinesList() {
|
||||
val text = if (filterPanel.isVisible) filterField.text else ""
|
||||
val selection = coroutinesList.selectedValue
|
||||
val model = coroutinesList.model as DefaultListModel<Any>
|
||||
model.clear()
|
||||
var selectedIndex = 0
|
||||
var index = 0
|
||||
val states = if (UISettings.instance.state.mergeEqualStackTraces) mergedDump else dump
|
||||
for (state in states) {
|
||||
if (StringUtil.containsIgnoreCase(state.stringStackTrace, text) || StringUtil.containsIgnoreCase(state.name, text)) {
|
||||
model.addElement(state)
|
||||
if (selection === state) {
|
||||
selectedIndex = index
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
if (!model.isEmpty) {
|
||||
coroutinesList.selectedIndex = selectedIndex
|
||||
}
|
||||
coroutinesList.revalidate()
|
||||
coroutinesList.repaint()
|
||||
}
|
||||
|
||||
internal fun highlightOccurrences(filter: String, project: Project, editor: Editor) {
|
||||
val highlightManager = HighlightManager.getInstance(project)
|
||||
val colorManager = EditorColorsManager.getInstance()
|
||||
val attributes = colorManager.globalScheme.getAttributes(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES)
|
||||
val documentText = editor.document.text
|
||||
var i = -1
|
||||
while (true) {
|
||||
val nextOccurrence = StringUtil.indexOfIgnoreCase(documentText, filter, i + 1)
|
||||
if (nextOccurrence < 0) {
|
||||
break
|
||||
}
|
||||
i = nextOccurrence
|
||||
highlightManager.addOccurrenceHighlight(
|
||||
editor, i, i + filter.length, attributes,
|
||||
HighlightManager.HIDE_BY_TEXT_CHANGE, null, null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getData(dataId: String): Any? = if (PlatformDataKeys.EXPORTER_TO_TEXT_FILE.`is`(dataId)) exporterToTextFile else null
|
||||
|
||||
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(infoData: CoroutineInfoData): SimpleTextAttributes {
|
||||
return when {
|
||||
infoData.isSuspended() -> SimpleTextAttributes.GRAY_ATTRIBUTES
|
||||
infoData.isEmptyStackTrace() -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter())
|
||||
else -> SimpleTextAttributes.REGULAR_ATTRIBUTES
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private inner class CoroutineListCellRenderer : ColoredListCellRenderer<Any>() {
|
||||
|
||||
override fun customizeCellRenderer(list: JList<*>, value: Any, index: Int, selected: Boolean, hasFocus: Boolean) {
|
||||
val state = value as CoroutineInfoData
|
||||
icon = getCoroutineStateIcon(state)
|
||||
val attrs = getAttributes(state)
|
||||
append(state.name + " (", attrs)
|
||||
var detail: String? = state.state.name
|
||||
if (detail == null) {
|
||||
detail = state.state.name
|
||||
}
|
||||
if (detail.length > 30) {
|
||||
detail = detail.substring(0, 30) + "..."
|
||||
}
|
||||
append(detail, attrs)
|
||||
append(")", attrs)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FilterAction : ToggleAction(
|
||||
"Filter",
|
||||
"Show only coroutines containing a specific string",
|
||||
AllIcons.General.Filter
|
||||
), DumbAware {
|
||||
|
||||
override fun isSelected(e: AnActionEvent): Boolean {
|
||||
return filterPanel.isVisible
|
||||
}
|
||||
|
||||
override fun setSelected(e: AnActionEvent, state: Boolean) {
|
||||
filterPanel.isVisible = state
|
||||
if (state) {
|
||||
IdeFocusManager.getInstance(AnAction.getEventProject(e)).requestFocus(filterField, true)
|
||||
filterField.selectText()
|
||||
}
|
||||
updateCoroutinesList()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class MergeStackTracesAction : ToggleAction(
|
||||
"Merge Identical Stacktraces",
|
||||
"Group coroutines with identical stacktraces",
|
||||
AllIcons.Actions.Collapseall
|
||||
), DumbAware {
|
||||
|
||||
override fun isSelected(e: AnActionEvent): Boolean {
|
||||
return UISettings.instance.state.mergeEqualStackTraces
|
||||
}
|
||||
|
||||
override fun setSelected(e: AnActionEvent, state: Boolean) {
|
||||
UISettings.instance.state.mergeEqualStackTraces = state
|
||||
updateCoroutinesList()
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
val buf = StringBuilder()
|
||||
buf.append("Full coroutine dump").append("\n\n")
|
||||
for (state in myCoroutinesDump) {
|
||||
buf.append(state.stringStackTrace).append("\n\n")
|
||||
}
|
||||
CopyPasteManager.getInstance().setContents(StringSelection(buf.toString()))
|
||||
|
||||
group.createNotification(
|
||||
"Full coroutine dump was successfully copied to clipboard",
|
||||
MessageType.INFO
|
||||
).notify(myProject)
|
||||
}
|
||||
|
||||
private val group = NotificationGroup.toolWindowGroup("Analyze coroutine dump", ToolWindowId.RUN, false)
|
||||
}
|
||||
|
||||
private class MyToFileExporter(
|
||||
private val myProject: Project,
|
||||
private val infoData: List<CoroutineInfoData>
|
||||
) : ExporterToTextFile {
|
||||
|
||||
override fun getReportText() = buildString {
|
||||
for (state in infoData)
|
||||
append(state.stringStackTrace).append("\n\n")
|
||||
}
|
||||
|
||||
override fun getDefaultFilePath() = (myProject.basePath ?: "") + File.separator + defaultReportFileName
|
||||
|
||||
override fun canExport() = infoData.isNotEmpty()
|
||||
|
||||
private val defaultReportFileName = "coroutines_report.txt"
|
||||
}
|
||||
}
|
||||
+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.coroutine.view
|
||||
|
||||
import com.intellij.debugger.engine.SuspendContextImpl
|
||||
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 com.sun.jdi.request.EventRequest
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
|
||||
|
||||
class CoroutineViewDebugSessionListener(
|
||||
private val session: XDebugSession,
|
||||
private val xCoroutineView: XCoroutineView
|
||||
) : XDebugSessionListener {
|
||||
val log by logger
|
||||
|
||||
override fun sessionPaused() {
|
||||
log.info("XListener: sessionPaused")
|
||||
val suspendContext = session.suspendContext ?: return requestClear()
|
||||
xCoroutineView.alarm.cancel()
|
||||
renew(suspendContext)
|
||||
}
|
||||
|
||||
override fun sessionResumed() {
|
||||
xCoroutineView.saveState()
|
||||
log.info("XListener: sessionResumed")
|
||||
val suspendContext = session.suspendContext ?: return requestClear()
|
||||
renew(suspendContext)
|
||||
}
|
||||
|
||||
override fun sessionStopped() {
|
||||
log.info("XListener: sessionStopped")
|
||||
val suspendContext = session.suspendContext ?: return requestClear()
|
||||
renew(suspendContext)
|
||||
}
|
||||
|
||||
override fun stackFrameChanged() {
|
||||
xCoroutineView.saveState()
|
||||
log.info("XListener: stackFrameChanged")
|
||||
// val suspendContext = session.suspendContext ?: return requestClear()
|
||||
// log.warn("stackFrameChanged ${session}")
|
||||
// renew(suspendContext)
|
||||
}
|
||||
|
||||
override fun beforeSessionResume() {
|
||||
log.info("XListener: beforeSessionResume")
|
||||
log.warn("beforeSessionResume ${session}")
|
||||
}
|
||||
|
||||
override fun settingsChanged() {
|
||||
log.info("XListener: settingsChanged")
|
||||
val suspendContext = session.suspendContext ?: return requestClear()
|
||||
log.warn("settingsChanged ${session}")
|
||||
renew(suspendContext)
|
||||
}
|
||||
|
||||
fun renew(suspendContext: XSuspendContext) {
|
||||
if(suspendContext is SuspendContextImpl && suspendContext.suspendPolicy == EventRequest.SUSPEND_ALL) {
|
||||
DebuggerUIUtil.invokeLater {
|
||||
xCoroutineView.renewRoot(suspendContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestClear() {
|
||||
if (ApplicationManager.getApplication().isUnitTestMode) { // no delay in tests
|
||||
xCoroutineView.resetRoot()
|
||||
} else {
|
||||
xCoroutineView.alarm.cancelAndRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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.coroutine.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.coroutine.command.CoroutineBuildCreationFrameCommand
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.command.CoroutineBuildFrameCommand
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.command.RefreshCoroutinesTreeCommand
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineDescriptorImpl
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CreationFramesDescriptor
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ManagerThreadExecutor
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Tree of coroutines for [CoroutinesPanel]
|
||||
*/
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
class CoroutinesDebuggerTree(project: Project) : ThreadsDebuggerTree(project) {
|
||||
// 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)
|
||||
context.debugProcess!!.managerThread.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
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.coroutine.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.coroutine.CoroutineDebuggerActions
|
||||
|
||||
/**
|
||||
* Added into ui in [CoroutineProjectConnectionListener.registerCoroutinesPanel]
|
||||
*/
|
||||
@Deprecated("moved to XCoroutineView")
|
||||
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"
|
||||
}
|
||||
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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.coroutine.view
|
||||
|
||||
import com.intellij.debugger.impl.DebuggerUtilsEx
|
||||
import com.intellij.debugger.settings.ThreadsViewSettings
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.editor.HighlighterColors
|
||||
import com.intellij.openapi.editor.colors.CodeInsightColors
|
||||
import com.intellij.openapi.editor.colors.TextAttributesKey
|
||||
import com.intellij.ui.ColoredTextContainer
|
||||
import com.intellij.ui.SimpleColoredComponent
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import com.intellij.xdebugger.frame.presentation.XValuePresentation
|
||||
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
|
||||
import com.intellij.xdebugger.impl.ui.XDebuggerUIConstants
|
||||
import com.sun.jdi.Location
|
||||
import com.sun.jdi.ReferenceType
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
|
||||
import javax.swing.Icon
|
||||
|
||||
class SimpleColoredTextIcon(val icon: Icon?, val hasChildrens: Boolean) {
|
||||
val texts = mutableListOf<String>()
|
||||
val textKeyAttributes = mutableListOf<TextAttributesKey>()
|
||||
|
||||
constructor(icon: Icon?, hasChildrens: Boolean, text: String) : this(icon, hasChildrens) {
|
||||
append(text)
|
||||
}
|
||||
|
||||
internal fun append(value: String) {
|
||||
texts.add(value)
|
||||
textKeyAttributes.add(CoroutineDebuggerColors.REGULAR_ATTRIBUTES)
|
||||
}
|
||||
|
||||
internal fun appendValue(value: String) {
|
||||
texts.add(value)
|
||||
textKeyAttributes.add(CoroutineDebuggerColors.VALUE_ATTRIBUTES)
|
||||
}
|
||||
|
||||
fun appendToComponent(component: ColoredTextContainer) {
|
||||
val size: Int = texts.size
|
||||
for (i in 0 until size) {
|
||||
val text: String = texts.get(i)
|
||||
val attribute: TextAttributesKey = textKeyAttributes.get(i)
|
||||
|
||||
component.append(text, when(attribute) {
|
||||
CoroutineDebuggerColors.REGULAR_ATTRIBUTES -> SimpleTextAttributes.REGULAR_ATTRIBUTES
|
||||
CoroutineDebuggerColors.VALUE_ATTRIBUTES -> XDebuggerUIConstants.VALUE_NAME_ATTRIBUTES
|
||||
else -> SimpleTextAttributes.REGULAR_ATTRIBUTES
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun forEachTextBlock(f: (Pair<String, TextAttributesKey>) -> Unit) {
|
||||
for (pair in texts zip textKeyAttributes)
|
||||
f(pair)
|
||||
}
|
||||
|
||||
fun simpleString(): String {
|
||||
val component = SimpleColoredComponent()
|
||||
appendToComponent(component)
|
||||
return component.getCharSequence(false).toString()
|
||||
}
|
||||
|
||||
fun valuePresentation(): XValuePresentation {
|
||||
return object : XValuePresentation() {
|
||||
override fun isShowName() = false
|
||||
|
||||
override fun getSeparator() = ""
|
||||
|
||||
override fun renderValue(renderer: XValueTextRenderer) {
|
||||
forEachTextBlock {
|
||||
renderer.renderValue(it.first, it.second)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CoroutineDebuggerColors {
|
||||
companion object {
|
||||
val REGULAR_ATTRIBUTES =
|
||||
TextAttributesKey.createTextAttributesKey("REGULAR_ATTRIBUTES", HighlighterColors.NO_HIGHLIGHTING)
|
||||
val VALUE_ATTRIBUTES =
|
||||
TextAttributesKey.createTextAttributesKey("VALUE_ATTRIBUTES", CodeInsightColors.WARNINGS_ATTRIBUTES)
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleColoredTextIconPresentationRenderer {
|
||||
private val settings: ThreadsViewSettings = ThreadsViewSettings.getInstance()
|
||||
|
||||
fun render(infoData: CoroutineInfoData): SimpleColoredTextIcon {
|
||||
val thread = infoData.activeThread
|
||||
val name = thread?.name()?.substringBefore(" @${infoData.name}") ?: ""
|
||||
val threadState = if (thread != null) DebuggerUtilsEx.getThreadStatusText(thread.status()) else ""
|
||||
|
||||
val icon = when (infoData.state) {
|
||||
CoroutineInfoData.State.SUSPENDED -> AllIcons.Debugger.ThreadSuspended
|
||||
CoroutineInfoData.State.RUNNING -> AllIcons.Debugger.ThreadRunning
|
||||
CoroutineInfoData.State.CREATED -> AllIcons.Debugger.ThreadStates.Idle
|
||||
}
|
||||
|
||||
val label = SimpleColoredTextIcon(icon, true)
|
||||
label.append("\"")
|
||||
label.appendValue(infoData.name)
|
||||
label.append("\": ${infoData.state}")
|
||||
if(name.isNotEmpty()) {
|
||||
label.append(" on thread \"")
|
||||
label.appendValue(name)
|
||||
label.append("\": $threadState")
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
/**
|
||||
* Taken from #StackFrameDescriptorImpl.calcRepresentation
|
||||
*/
|
||||
fun render(location: Location): SimpleColoredTextIcon {
|
||||
val label = SimpleColoredTextIcon(null, false)
|
||||
DebuggerUIUtil.getColorScheme(null)
|
||||
if (location.method() != null) {
|
||||
val myName = location.method().name()
|
||||
val methodDisplay = if (settings.SHOW_ARGUMENTS_TYPES)
|
||||
DebuggerUtilsEx.methodNameWithArguments(location.method())
|
||||
else
|
||||
myName
|
||||
label.appendValue(methodDisplay)
|
||||
}
|
||||
if (settings.SHOW_LINE_NUMBER) {
|
||||
label.append(":")
|
||||
label.append("" + DebuggerUtilsEx.getLineNumber(location, false))
|
||||
}
|
||||
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(", ")
|
||||
val dotIndex = name.lastIndexOf('.')
|
||||
if (dotIndex < 0) {
|
||||
label.append(name)
|
||||
} else {
|
||||
label.append(name.substring(dotIndex + 1))
|
||||
if (settings.SHOW_PACKAGE_NAME) {
|
||||
label.append(" (${name.substring( 0, dotIndex)})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (settings.SHOW_SOURCE_NAME) {
|
||||
label.append(", ")
|
||||
val sourceName = DebuggerUtilsEx.getSourceName(location) { e: Throwable? -> "Unknown Source" }
|
||||
label.append(sourceName)
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
fun renderCreationNode(infoData: CoroutineInfoData) =
|
||||
SimpleColoredTextIcon(AllIcons.Debugger.ThreadSuspended, true, "Creation stack frame of ${infoData.name}")
|
||||
|
||||
fun renderErrorNode(error: String) =
|
||||
SimpleColoredTextIcon(AllIcons.Actions.Lightning,false, error)
|
||||
|
||||
fun renderRoorNode(text: String) =
|
||||
SimpleColoredTextIcon(null, true, text)
|
||||
|
||||
fun renderGroup(groupName: String) =
|
||||
SimpleColoredTextIcon(AllIcons.Debugger.ThreadGroup,true, groupName)
|
||||
}
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* 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.coroutine.view
|
||||
|
||||
import com.intellij.debugger.DebuggerManagerEx
|
||||
import com.intellij.debugger.engine.DebugProcessImpl
|
||||
import com.intellij.debugger.engine.JavaDebugProcess
|
||||
import com.intellij.debugger.engine.JavaExecutionStack
|
||||
import com.intellij.debugger.engine.SuspendContextImpl
|
||||
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.JavaPsiFacade
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import com.intellij.ui.DoubleClickListener
|
||||
import com.intellij.ui.OnePixelSplitter
|
||||
import com.intellij.util.SingleAlarm
|
||||
import com.intellij.xdebugger.XDebugSession
|
||||
import com.intellij.xdebugger.XDebuggerUtil
|
||||
import com.intellij.xdebugger.XSourcePosition
|
||||
import com.intellij.xdebugger.frame.*
|
||||
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
|
||||
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree
|
||||
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreePanel
|
||||
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeRestorer
|
||||
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeState
|
||||
import com.intellij.xdebugger.impl.ui.tree.nodes.XValueContainerNode
|
||||
import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl
|
||||
import com.sun.jdi.ClassType
|
||||
import javaslang.control.Either
|
||||
import org.jetbrains.kotlin.idea.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineAsyncStackTraceProvider
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineDebuggerContentInfo
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineDebuggerContentInfo.Companion.XCOROUTINE_POPUP_ACTION_GROUP
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.command.*
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.SuspendStackFrameDescriptor
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.data.SyntheticStackFrame
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ApplicationThreadExecutor
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.AsyncStackTraceContext
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.CoroutineDebugProbesProxy
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ManagerThreadExecutor
|
||||
import org.jetbrains.kotlin.idea.debugger.coroutine.util.*
|
||||
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||
import java.awt.event.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import java.awt.event.MouseEvent
|
||||
|
||||
|
||||
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 { resetRoot() }, VIEW_CLEAR_DELAY, this)
|
||||
val javaDebugProcess = session.debugProcess as JavaDebugProcess
|
||||
val debugProcess: DebugProcessImpl = javaDebugProcess.debuggerSession.process
|
||||
val renderer = SimpleColoredTextIconPresentationRenderer()
|
||||
val managerThreadExecutor = ManagerThreadExecutor(debugProcess)
|
||||
val applicationThreadExecutor = ApplicationThreadExecutor()
|
||||
var treeState: XDebuggerTreeState? = null
|
||||
private var restorer: XDebuggerTreeRestorer? = null
|
||||
private var selectedNodeListener = XDebuggerTreeSelectedNodeListener(panel.tree)
|
||||
|
||||
companion object {
|
||||
private val VIEW_CLEAR_DELAY = 100 //ms
|
||||
}
|
||||
|
||||
init {
|
||||
splitter.firstComponent = panel.mainPanel
|
||||
selectedNodeListener.installOn()
|
||||
}
|
||||
|
||||
|
||||
fun saveState() {
|
||||
DebuggerUIUtil.invokeLater {
|
||||
if (! (panel.tree.root is EmptyNode)) {
|
||||
treeState = XDebuggerTreeState.saveState(panel.tree)
|
||||
log.info("Tree state saved")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetRoot() {
|
||||
DebuggerUIUtil.invokeLater {
|
||||
panel.tree.setRoot(EmptyNode(), false)
|
||||
}
|
||||
}
|
||||
|
||||
fun renewRoot(suspendContext: XSuspendContext) {
|
||||
panel.tree.setRoot(XCoroutinesRootNode(suspendContext), false)
|
||||
if(treeState != null) {
|
||||
restorer?.dispose()
|
||||
restorer = treeState?.restoreState(panel.tree)
|
||||
log.info("Tree state restored")
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
restorer?.dispose()
|
||||
}
|
||||
|
||||
fun forceClear() {
|
||||
alarm.cancel()
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
inner class EmptyNode : XValueContainerNode<XValueContainer>(panel.tree, null, true, object : XValueContainer() {})
|
||||
|
||||
inner class XCoroutinesRootNode(suspendContext: XSuspendContext) :
|
||||
XValueContainerNode<CoroutineGroupContainer>(panel.tree, null, false, CoroutineGroupContainer(suspendContext, "Default group"))
|
||||
|
||||
inner class CoroutineGroupContainer(val suspendContext: XSuspendContext, val groupName: String) : XValueContainer() {
|
||||
override fun computeChildren(node: XCompositeNode) {
|
||||
val groups = XValueChildrenList.singleton(CoroutineContainer(suspendContext, groupName))
|
||||
node.addChildren(groups, true)
|
||||
}
|
||||
}
|
||||
|
||||
inner class CoroutineContainer(
|
||||
val suspendContext: XSuspendContext,
|
||||
val groupName: String
|
||||
) : RendererContainer(renderer.renderGroup(groupName)) {
|
||||
|
||||
override fun computeChildren(node: XCompositeNode) {
|
||||
managerThreadExecutor.on(suspendContext).schedule {
|
||||
val debugProbesProxy = CoroutineDebugProbesProxy(suspendContext)
|
||||
|
||||
var coroutineCache = debugProbesProxy.dumpCoroutines()
|
||||
if (coroutineCache.isOk()) {
|
||||
val children = XValueChildrenList()
|
||||
coroutineCache.cache.forEach {
|
||||
children.add(FramesContainer(it, suspendContext))
|
||||
}
|
||||
node.addChildren(children, true)
|
||||
} else {
|
||||
node.addChildren(XValueChildrenList.singleton(ErrorNode("Error occured while fetching information")), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ErrorNode(val error: String) : RendererContainer(renderer.renderErrorNode(error))
|
||||
|
||||
inner class FramesContainer(
|
||||
private val infoData: CoroutineInfoData,
|
||||
private val suspendContext: XSuspendContext
|
||||
) : RendererContainer(renderer.render(infoData)) {
|
||||
|
||||
override fun computeChildren(node: XCompositeNode) {
|
||||
managerThreadExecutor.on(suspendContext).schedule {
|
||||
val debugProbesProxy = CoroutineDebugProbesProxy(suspendContext)
|
||||
val children = XValueChildrenList()
|
||||
debugProbesProxy.frameBuilder().build(infoData)
|
||||
val creationStack = mutableListOf<CreationCoroutineStackFrameItem>()
|
||||
infoData.stackFrameList.forEach {
|
||||
if (it is CreationCoroutineStackFrameItem)
|
||||
creationStack.add(it)
|
||||
else
|
||||
children.add(CoroutineFrameValue(it))
|
||||
}
|
||||
children.add(CreationFramesContainer(infoData, creationStack))
|
||||
node.addChildren(children, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class CreationFramesContainer(
|
||||
private val infoData: CoroutineInfoData,
|
||||
private val creationFrames: List<CreationCoroutineStackFrameItem>
|
||||
) : RendererContainer(renderer.renderCreationNode(infoData)) {
|
||||
|
||||
override fun computeChildren(node: XCompositeNode) {
|
||||
val children = XValueChildrenList()
|
||||
|
||||
creationFrames.forEach {
|
||||
children.add(CoroutineFrameValue(it))
|
||||
}
|
||||
node.addChildren(children, true)
|
||||
}
|
||||
}
|
||||
|
||||
inner class CoroutineFrameValue(val frame: CoroutineStackFrameItem
|
||||
) : XNamedValue(frame.uniqueId()) {
|
||||
override fun computePresentation(node: XValueNode, place: XValuePlace) =
|
||||
applyRenderer(node, renderer.render(frame.location()))
|
||||
}
|
||||
|
||||
private fun applyRenderer(node: XValueNode, presentation: SimpleColoredTextIcon) =
|
||||
node.setPresentation(presentation.icon, presentation.valuePresentation(), presentation.hasChildrens)
|
||||
|
||||
open inner class RendererContainer(val presentation: SimpleColoredTextIcon) : XNamedValue(presentation.simpleString()) {
|
||||
override fun computePresentation(node: XValueNode, place: XValuePlace) =
|
||||
applyRenderer(node, presentation)
|
||||
}
|
||||
|
||||
inner class XDebuggerTreeSelectedNodeListener(val tree: XDebuggerTree) {
|
||||
|
||||
fun installOn() {
|
||||
object : DoubleClickListener() {
|
||||
override fun onDoubleClick(e: MouseEvent) =
|
||||
nodeSelected(Either.left(e))
|
||||
}.installOn(tree)
|
||||
|
||||
tree.addKeyListener(object : KeyAdapter() {
|
||||
override fun keyPressed(e: KeyEvent) {
|
||||
val key = e.keyCode
|
||||
if (key == KeyEvent.VK_ENTER || key == KeyEvent.VK_SPACE || key == KeyEvent.VK_RIGHT)
|
||||
nodeSelected(Either.right(e))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun nodeSelected(event: Either<MouseEvent, KeyEvent>) : Boolean {
|
||||
val selectedNodes = tree.getSelectedNodes(XValueNodeImpl::class.java, null)
|
||||
if (selectedNodes.size == 1) {
|
||||
val node = selectedNodes[0]
|
||||
val valueContainer = node.valueContainer
|
||||
if (valueContainer is XCoroutineView.CoroutineFrameValue) {
|
||||
val frame = valueContainer.frame
|
||||
val threadSuspendContext = session.suspendContext as SuspendContextImpl
|
||||
when (frame) {
|
||||
is RunningCoroutineStackFrameItem -> {
|
||||
val threadProxy = valueContainer.frame.frame.threadProxy()
|
||||
val isCurrentContext = threadSuspendContext.thread == threadProxy
|
||||
createStackAndSetFrame(threadProxy, { frame.stackFrame }, isCurrentContext)
|
||||
}
|
||||
is CreationCoroutineStackFrameItem -> {
|
||||
val position = getPosition(frame.stackTraceElement) ?: return false
|
||||
val threadProxy = threadSuspendContext.thread as ThreadReferenceProxyImpl
|
||||
val stackFrame =
|
||||
createStackAndSetFrame(threadProxy, { SyntheticStackFrame(frame.emptyDescriptor(), emptyList(), position) })
|
||||
}
|
||||
is SuspendCoroutineStackFrameItem -> {
|
||||
val position = getPosition(frame.stackTraceElement) ?: return false
|
||||
}
|
||||
is AsyncCoroutineStackFrameItem -> {
|
||||
|
||||
}
|
||||
// else -> {
|
||||
// val (stack, stackFrame) = createSyntheticStackFrame(descriptor, pos) ?: return
|
||||
// val action: () -> Unit = { context.debuggerSession?.xDebugSession?.setCurrentStackFrame(stack, stackFrame) }
|
||||
// - ApplicationManager.getApplication()
|
||||
// - .invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree))
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun createSyntheticStackFrame() {}
|
||||
|
||||
fun createStackAndSetFrame(threadReferenceProxy: ThreadReferenceProxyImpl, stackFrameProvider: () -> XStackFrame, isCurrentContext: Boolean = false) {
|
||||
val threadSuspendContext = session.suspendContext as SuspendContextImpl
|
||||
managerThreadExecutor.on(threadSuspendContext).schedule {
|
||||
val stackFrame = stackFrameProvider.invoke()
|
||||
val executionStack = createExecutionStack(threadReferenceProxy, isCurrentContext)
|
||||
applicationThreadExecutor.schedule(
|
||||
{
|
||||
session.setCurrentStackFrame(executionStack, stackFrame)
|
||||
}, panel.tree)
|
||||
}
|
||||
}
|
||||
|
||||
fun createExecutionStack(proxy: ThreadReferenceProxyImpl, isCurrentContext: Boolean = false) : XExecutionStack {
|
||||
val executionStack = CoroutineDebuggerExecutionStack(proxy, isCurrentContext)
|
||||
executionStack.initTopFrame()
|
||||
return executionStack
|
||||
}
|
||||
|
||||
inner class CoroutineDebuggerExecutionStack(threadReferenceProxy: ThreadReferenceProxyImpl, isCurrentContext: Boolean) :
|
||||
JavaExecutionStack(threadReferenceProxy, debugProcess, isCurrentContext)
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 vars = with(CoroutineAsyncStackTraceProvider()) {
|
||||
AsyncStackTraceContext(
|
||||
execContext,
|
||||
aMethod
|
||||
).getSpilledVariables(continuation)
|
||||
} ?: return null
|
||||
return executionStack to SyntheticStackFrame(descriptor, vars, pos)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user