coroutine debugger logic moved to jvm-debugger-coroutine module

This commit is contained in:
Vladimir Ilmov
2020-01-08 11:53:50 +01:00
committed by Vladimir Ilmov
parent b1b0817336
commit e570450c59
37 changed files with 129 additions and 107 deletions
@@ -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() }
}
@@ -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
}
@@ -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)
}
}
}
@@ -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)
}
}
}
@@ -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"
}
}
@@ -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")
}
@@ -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)
}
}
}
@@ -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)
}
}
@@ -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
)
}
}
@@ -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()
}
}
}
@@ -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
}
}
@@ -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()
}
}
}
@@ -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)
@@ -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
}
}
@@ -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()
}
}
@@ -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
}
@@ -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)
}
@@ -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)
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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() }
}
}
@@ -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)
}
@@ -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
}
}
@@ -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();
}
}
@@ -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"
}
}
@@ -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()
}
}
}
@@ -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
}
@@ -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"
}
}
@@ -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)
}
@@ -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)
}
}