Debugger: Coroutines stack frames with variables & coroutine dumps
This commit is contained in:
committed by
Vladimir Ilmov
parent
c459b2ca6e
commit
5975251a32
@@ -55,6 +55,8 @@ import org.jetbrains.kotlin.idea.conversion.copy.AbstractLiteralKotlinToKotlinCo
|
|||||||
import org.jetbrains.kotlin.idea.conversion.copy.AbstractLiteralTextToKotlinCopyPasteTest
|
import org.jetbrains.kotlin.idea.conversion.copy.AbstractLiteralTextToKotlinCopyPasteTest
|
||||||
import org.jetbrains.kotlin.idea.conversion.copy.AbstractTextJavaToKotlinCopyPasteConversionTest
|
import org.jetbrains.kotlin.idea.conversion.copy.AbstractTextJavaToKotlinCopyPasteConversionTest
|
||||||
import org.jetbrains.kotlin.idea.coverage.AbstractKotlinCoverageOutputFilesTest
|
import org.jetbrains.kotlin.idea.coverage.AbstractKotlinCoverageOutputFilesTest
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.*
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.coroutines.AbstractCoroutineDumpTest
|
||||||
import org.jetbrains.kotlin.idea.debugger.evaluate.*
|
import org.jetbrains.kotlin.idea.debugger.evaluate.*
|
||||||
import org.jetbrains.kotlin.idea.debugger.test.sequence.exec.AbstractSequenceTraceTestCase
|
import org.jetbrains.kotlin.idea.debugger.test.sequence.exec.AbstractSequenceTraceTestCase
|
||||||
import org.jetbrains.kotlin.idea.debugger.test.*
|
import org.jetbrains.kotlin.idea.debugger.test.*
|
||||||
|
|||||||
+2
-2
@@ -140,7 +140,7 @@ class KotlinCoroutinesAsyncStackTraceProvider : KotlinCoroutinesAsyncStackTraceP
|
|||||||
return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber)
|
return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun AsyncStackTraceContext.getSpilledVariables(continuation: ObjectReference): List<XNamedValue>? {
|
fun AsyncStackTraceContext.getSpilledVariables(continuation: ObjectReference): List<XNamedValue>? {
|
||||||
val getSpilledVariableFieldMappingMethod = debugMetadataKtType.methodsByName(
|
val getSpilledVariableFieldMappingMethod = debugMetadataKtType.methodsByName(
|
||||||
"getSpilledVariableFieldMapping",
|
"getSpilledVariableFieldMapping",
|
||||||
"(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;"
|
"(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;"
|
||||||
@@ -188,7 +188,7 @@ class KotlinCoroutinesAsyncStackTraceProvider : KotlinCoroutinesAsyncStackTraceP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class AsyncStackTraceContext(
|
class AsyncStackTraceContext(
|
||||||
val context: ExecutionContext,
|
val context: ExecutionContext,
|
||||||
val method: Method,
|
val method: Method,
|
||||||
val debugMetadataKtType: ClassType
|
val debugMetadataKtType: ClassType
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||||
|
|
||||||
|
import com.intellij.debugger.DebuggerManagerEx
|
||||||
|
import com.intellij.debugger.engine.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.diagnostic.Logger
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.openapi.ui.MessageType
|
||||||
|
import com.intellij.openapi.util.Disposer
|
||||||
|
import com.intellij.openapi.util.registry.Registry
|
||||||
|
import com.intellij.psi.search.GlobalSearchScope
|
||||||
|
import com.intellij.util.text.DateFormatUtil
|
||||||
|
import com.intellij.xdebugger.impl.XDebuggerManagerImpl
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.evaluate.createExecutionContext
|
||||||
|
|
||||||
|
@Suppress("ComponentNotRegistered")
|
||||||
|
class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate {
|
||||||
|
private val logger = Logger.getInstance(this::class.java)
|
||||||
|
|
||||||
|
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 execContext = context.createExecutionContext() ?: return
|
||||||
|
val states = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
|
||||||
|
if (states.isLeft) {
|
||||||
|
logger.warn(states.left)
|
||||||
|
XDebuggerManagerImpl.NOTIFICATION_GROUP
|
||||||
|
.createNotification(
|
||||||
|
"Coroutine dump failed. See log",
|
||||||
|
MessageType.ERROR
|
||||||
|
).notify(project)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val f = fun() {
|
||||||
|
addCoroutineDump(
|
||||||
|
project,
|
||||||
|
states.get(),
|
||||||
|
session.xDebugSession?.ui ?: return,
|
||||||
|
session.searchScope
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ApplicationManager.getApplication().invokeLater(f, ModalityState.NON_MODAL)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analog of [DebuggerUtilsEx.addThreadDump].
|
||||||
|
*/
|
||||||
|
fun addCoroutineDump(project: Project, coroutines: List<CoroutineState>, 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 && Registry.`is`("kotlin.debugger" + ".coroutines")
|
||||||
|
presentation.isVisible = presentation.isEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
+300
@@ -0,0 +1,300 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||||
|
|
||||||
|
import com.intellij.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 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<CoroutineState>) :
|
||||||
|
JPanel(BorderLayout()), DataProvider {
|
||||||
|
private var exporterToTextFile: ExporterToTextFile
|
||||||
|
private var mergedDump = ArrayList<CoroutineState>()
|
||||||
|
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 CoroutineState
|
||||||
|
AnalyzeStacktraceUtil.printStacktrace(consoleView, selection.stringStackTrace)
|
||||||
|
} else {
|
||||||
|
AnalyzeStacktraceUtil.printStacktrace(consoleView, "")
|
||||||
|
}
|
||||||
|
repaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exporterToTextFile = createToFileExporter(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(state: CoroutineState): Icon {
|
||||||
|
return when (state.state) {
|
||||||
|
CoroutineState.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign)
|
||||||
|
CoroutineState.State.SUSPENDED -> AllIcons.Actions.Pause
|
||||||
|
else -> EmptyIcon.create(6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAttributes(state: CoroutineState): SimpleTextAttributes {
|
||||||
|
return when {
|
||||||
|
state.isSuspended -> SimpleTextAttributes.GRAY_ATTRIBUTES
|
||||||
|
state.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 CoroutineState
|
||||||
|
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 threads 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private inner class MergeStacktracesAction :
|
||||||
|
ToggleAction(
|
||||||
|
"Merge Identical Stacktraces",
|
||||||
|
"Group coroutines with identical stacktraces",
|
||||||
|
AllIcons.General.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<CoroutineState>, 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 fun createToFileExporter(project: Project, states: List<CoroutineState>): ExporterToTextFile {
|
||||||
|
return MyToFileExporter(project, states)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MyToFileExporter(private val myProject: Project, private val states: List<CoroutineState>) :
|
||||||
|
ExporterToTextFile {
|
||||||
|
|
||||||
|
override fun getReportText(): String {
|
||||||
|
val sb = StringBuilder()
|
||||||
|
for (state in states) {
|
||||||
|
sb.append(state.stringStackTrace).append("\n\n")
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun getDefaultFilePath(): String {
|
||||||
|
val baseDir = myProject.baseDir
|
||||||
|
return if (baseDir != null) {
|
||||||
|
baseDir.presentableUrl + File.separator + defaultReportFileName
|
||||||
|
} else ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canExport(): Boolean {
|
||||||
|
return states.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val defaultReportFileName = "coroutines_report.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||||
|
|
||||||
|
import com.sun.jdi.*
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.isSubtype
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents state of a coroutine.
|
||||||
|
* @see `kotlinx.coroutines.debug.CoroutineInfo`
|
||||||
|
*/
|
||||||
|
class CoroutineState(
|
||||||
|
val name: String,
|
||||||
|
val state: State,
|
||||||
|
val thread: ThreadReference? = null,
|
||||||
|
val stackTrace: List<StackTraceElement>,
|
||||||
|
val frame: ObjectReference?
|
||||||
|
) {
|
||||||
|
val isSuspended: Boolean = state == State.SUSPENDED
|
||||||
|
val isEmptyStackTrace: Boolean by lazy { stackTrace.isEmpty() }
|
||||||
|
val stringStackTrace: String by lazy {
|
||||||
|
buildString {
|
||||||
|
appendln("\"$name\", state: $state")
|
||||||
|
stackTrace.forEach {
|
||||||
|
appendln("\t$it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl)
|
||||||
|
* @return null if given ObjectReference is not a BaseContinuationImpl instance or completion is null
|
||||||
|
*/
|
||||||
|
private fun getNextFrame(continuation: ObjectReference, context: ExecutionContext): ObjectReference? {
|
||||||
|
val type = continuation.type() as ClassType
|
||||||
|
if (!type.isSubtype("kotlin.coroutines.jvm.internal.BaseContinuationImpl")) return null
|
||||||
|
val next = type.concreteMethodByName("getCompletion", "()Lkotlin/coroutines/Continuation;")
|
||||||
|
return context.invokeMethod(continuation, next, emptyList()) as? ObjectReference
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find continuation for the [stackTraceElement]
|
||||||
|
* Gets current CoroutineInfo.lastObservedFrame and finds next frames in it until null or needed stackTraceElement is found
|
||||||
|
* @return null if matching continuation is not found or is not BaseContinuationImpl
|
||||||
|
*/
|
||||||
|
fun getContinuation(stackTraceElement: StackTraceElement, context: ExecutionContext): ObjectReference? {
|
||||||
|
var continuation = frame ?: return null
|
||||||
|
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 ""
|
||||||
|
}
|
||||||
|
val lineNumber = {
|
||||||
|
val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference
|
||||||
|
if (trace != null)
|
||||||
|
(context.invokeMethod(trace, getLineNumber, emptyList()) as IntegerValue).value()
|
||||||
|
else -239 // invalid line number (but well-educated)
|
||||||
|
}
|
||||||
|
|
||||||
|
while (continuation.type().isSubtype(baseType)
|
||||||
|
&& (stackTraceElement.className != className() || stackTraceElement.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
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class State {
|
||||||
|
RUNNING,
|
||||||
|
SUSPENDED,
|
||||||
|
CREATED
|
||||||
|
}
|
||||||
|
}
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||||
|
|
||||||
|
import com.intellij.debugger.DebuggerInvocationUtil
|
||||||
|
import com.intellij.debugger.DebuggerManagerEx
|
||||||
|
import com.intellij.debugger.actions.ThreadDumpAction
|
||||||
|
import com.intellij.debugger.impl.DebuggerSession
|
||||||
|
import com.intellij.execution.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
|
||||||
|
import com.intellij.execution.ui.RunnerLayoutUi
|
||||||
|
import com.intellij.execution.ui.layout.PlaceInGrid
|
||||||
|
import com.intellij.execution.ui.layout.impl.RunnerContentUi
|
||||||
|
import com.intellij.execution.ui.layout.impl.RunnerLayoutUiImpl
|
||||||
|
import com.intellij.icons.AllIcons
|
||||||
|
import com.intellij.openapi.actionSystem.ActionManager
|
||||||
|
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||||
|
import com.intellij.openapi.actionSystem.DefaultActionGroup
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.openapi.util.Key
|
||||||
|
import com.intellij.openapi.util.registry.Registry
|
||||||
|
import com.intellij.ui.content.ContentManagerAdapter
|
||||||
|
import com.intellij.ui.content.ContentManagerEvent
|
||||||
|
import com.intellij.util.messages.MessageBusConnection
|
||||||
|
import com.intellij.xdebugger.XDebugProcess
|
||||||
|
import com.intellij.xdebugger.XDebuggerManager
|
||||||
|
import com.intellij.xdebugger.XDebuggerManagerListener
|
||||||
|
import org.jetbrains.kotlin.psi.UserDataProperty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs coroutines debug agent and coroutines tab if `kotlinx.coroutines.debug` dependency is found
|
||||||
|
*/
|
||||||
|
@Suppress("IncompatibleAPI")
|
||||||
|
class CoroutinesDebugConfigurationExtension : RunConfigurationExtension() {
|
||||||
|
private var Project.listenerCreated: Boolean? by UserDataProperty(Key.create("COROUTINES_DEBUG_TAB_CREATE_LISTENER"))
|
||||||
|
|
||||||
|
override fun isApplicableFor(configuration: RunConfigurationBase<*>): Boolean {
|
||||||
|
return Registry.`is`("kotlin.debugger" + ".coroutines")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : RunConfigurationBase<*>?> updateJavaParameters(
|
||||||
|
configuration: T,
|
||||||
|
params: JavaParameters?,
|
||||||
|
runnerSettings: RunnerSettings?
|
||||||
|
) {
|
||||||
|
if (!Registry.`is`("kotlin.debugger" + ".coroutines")) return
|
||||||
|
if (runnerSettings is DebuggingRunnerData && params != null
|
||||||
|
&& params.classPath != null
|
||||||
|
&& params.classPath.pathList.isNotEmpty()
|
||||||
|
) {
|
||||||
|
params.classPath.pathList.forEach {
|
||||||
|
if (!it.contains("kotlinx-coroutines-debug")) return@forEach
|
||||||
|
// if debug library is included into project, add agent which installs probes
|
||||||
|
params.vmParametersList?.add("-javaagent:$it")
|
||||||
|
params.vmParametersList?.add("-ea")
|
||||||
|
val project = (configuration as RunConfigurationBase<*>).project
|
||||||
|
// add listener to put coroutines tab into debugger tab
|
||||||
|
if (project.listenerCreated != true) { // prevent multiple listeners creation
|
||||||
|
val connection = project.messageBus.connect()
|
||||||
|
connection.subscribe(
|
||||||
|
XDebuggerManager.TOPIC, createListener(project, connection)
|
||||||
|
)
|
||||||
|
project.listenerCreated = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createListener(project: Project, connection: MessageBusConnection): XDebuggerManagerListener {
|
||||||
|
return object : XDebuggerManagerListener {
|
||||||
|
override fun processStarted(debugProcess: XDebugProcess) {
|
||||||
|
DebuggerInvocationUtil.swingInvokeLater(project) {
|
||||||
|
val session = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession
|
||||||
|
if (session != null)
|
||||||
|
registerCoroutinesPanel(session.xDebugSession?.ui ?: return@swingInvokeLater, session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun processStopped(debugProcess: XDebugProcess) {
|
||||||
|
connection.disconnect()
|
||||||
|
project.listenerCreated = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds panel to XDebugSessionTab
|
||||||
|
*/
|
||||||
|
private fun registerCoroutinesPanel(ui: RunnerLayoutUi, session: DebuggerSession) {
|
||||||
|
val panel = CoroutinesPanel(session.project, session.contextManager)
|
||||||
|
val content = ui.createContent(
|
||||||
|
"CoroutinesContent", panel, "Coroutines", // TODO(design)
|
||||||
|
AllIcons.Debugger.ThreadGroup, null
|
||||||
|
)
|
||||||
|
content.isCloseable = false
|
||||||
|
ui.addContent(content, 0, PlaceInGrid.left, true)
|
||||||
|
ui.addListener(object : ContentManagerAdapter() {
|
||||||
|
override fun selectionChanged(event: ContentManagerEvent) {
|
||||||
|
if (event.content === content) {
|
||||||
|
if (content.isSelected) {
|
||||||
|
panel.setUpdateEnabled(true)
|
||||||
|
if (panel.isRefreshNeeded) {
|
||||||
|
panel.rebuildIfVisible(DebuggerSession.Event.CONTEXT)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panel.setUpdateEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, content)
|
||||||
|
// add coroutine dump button: due to api problem left toolbar is copied, modified and reset to tab
|
||||||
|
val runnerContent = (ui.options as RunnerLayoutUiImpl).getData(RunnerContentUi.KEY.name) as RunnerContentUi
|
||||||
|
val modifiedActions = runnerContent.getActions(true)
|
||||||
|
val pos = modifiedActions.indexOfLast { it is ThreadDumpAction }
|
||||||
|
modifiedActions.add(pos + 1, ActionManager.getInstance().getAction("Kotlin.XDebugger.CoroutinesDump"))
|
||||||
|
ui.options.setLeftToolbar(DefaultActionGroup(modifiedActions), ActionPlaces.DEBUGGER_TOOLBAR)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||||
|
|
||||||
|
import com.intellij.debugger.engine.DebugProcess
|
||||||
|
import com.intellij.openapi.util.Key
|
||||||
|
import com.sun.jdi.*
|
||||||
|
import com.sun.tools.jdi.StringReferenceImpl
|
||||||
|
import javaslang.control.Either
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||||
|
import org.jetbrains.kotlin.psi.UserDataProperty
|
||||||
|
|
||||||
|
object CoroutinesDebugProbesProxy {
|
||||||
|
private const val DEBUG_PACKAGE = "kotlinx.coroutines.debug"
|
||||||
|
private var DebugProcess.references by UserDataProperty(Key.create<ProcessReferences>("COROUTINES_DEBUG_REFERENCES"))
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun install(context: ExecutionContext) {
|
||||||
|
val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType
|
||||||
|
val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference }
|
||||||
|
val install = debugProbes.concreteMethodByName("install", "()V")
|
||||||
|
context.invokeMethod(instance, install, emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
fun uninstall(context: ExecutionContext) {
|
||||||
|
val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType
|
||||||
|
val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference }
|
||||||
|
val uninstall = debugProbes.concreteMethodByName("uninstall", "()V")
|
||||||
|
context.invokeMethod(instance, uninstall, emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes DebugProbes from debugged process's classpath and returns states of coroutines
|
||||||
|
* Should be invoked on debugger manager thread
|
||||||
|
*/
|
||||||
|
fun dumpCoroutines(context: ExecutionContext): Either<Throwable, List<CoroutineState>> {
|
||||||
|
try {
|
||||||
|
if (context.debugProcess.references == null) {
|
||||||
|
context.debugProcess.references = ProcessReferences(context)
|
||||||
|
}
|
||||||
|
val refs = context.debugProcess.references!! // already initialized if it was null
|
||||||
|
|
||||||
|
// get dump
|
||||||
|
val infoList = context.invokeMethod(refs.instance, refs.dumpMethod, emptyList()) as ObjectReference
|
||||||
|
context.keepReference(infoList)
|
||||||
|
val size = (context.invokeMethod(infoList, refs.getSize, emptyList()) as IntegerValue).value()
|
||||||
|
|
||||||
|
return Either.right(List(size) {
|
||||||
|
val index = context.vm.mirrorOf(it)
|
||||||
|
val elem = context.invokeMethod(infoList, refs.getElement, listOf(index)) as ObjectReference
|
||||||
|
val name = getName(context, elem, refs)
|
||||||
|
val state = getState(context, elem, refs)
|
||||||
|
val thread = getLastObservedThread(elem, refs.threadRef)
|
||||||
|
CoroutineState(
|
||||||
|
name,
|
||||||
|
CoroutineState.State.valueOf(state),
|
||||||
|
thread,
|
||||||
|
getStackTrace(elem, refs, context),
|
||||||
|
elem.getValue(refs.continuation) as? ObjectReference
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
return Either.left(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getName(
|
||||||
|
context: ExecutionContext, // Execution context to invoke methods
|
||||||
|
info: ObjectReference, // CoroutineInfo instance
|
||||||
|
refs: ProcessReferences
|
||||||
|
): String {
|
||||||
|
// equals to `coroutineInfo.context.get(CoroutineName).name`
|
||||||
|
val coroutineContextInst = context.invokeMethod(info, refs.getContext, emptyList()) as ObjectReference
|
||||||
|
val coroutineName = context.invokeMethod(
|
||||||
|
coroutineContextInst,
|
||||||
|
refs.getContextElement, listOf(refs.nameKey)
|
||||||
|
) as? ObjectReference
|
||||||
|
// If the coroutine doesn't have a given name, CoroutineContext.get(CoroutineName) returns null
|
||||||
|
val name = if (coroutineName != null) (context.invokeMethod(
|
||||||
|
coroutineName,
|
||||||
|
refs.getName, emptyList()
|
||||||
|
) as StringReferenceImpl).value() else "coroutine"
|
||||||
|
val id = (info.getValue(refs.idField) as LongValue).value()
|
||||||
|
return "$name#$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getState(
|
||||||
|
context: ExecutionContext, // Execution context to invoke methods
|
||||||
|
info: ObjectReference, // CoroutineInfo instance
|
||||||
|
refs: ProcessReferences
|
||||||
|
): String {
|
||||||
|
// equals to stringState = coroutineInfo.state.toString()
|
||||||
|
val state = context.invokeMethod(info, refs.getState, emptyList()) as ObjectReference
|
||||||
|
return (context.invokeMethod(state, refs.toString, emptyList()) as StringReferenceImpl).value()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLastObservedThread(
|
||||||
|
info: ObjectReference, // CoroutineInfo instance
|
||||||
|
threadRef: Field // reference to lastObservedThread
|
||||||
|
): ThreadReference? = info.getValue(threadRef) as ThreadReference?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of stackTraceElements for the given CoroutineInfo's [ObjectReference]
|
||||||
|
*/
|
||||||
|
private fun getStackTrace(
|
||||||
|
info: ObjectReference,
|
||||||
|
refs: ProcessReferences,
|
||||||
|
context: ExecutionContext
|
||||||
|
): List<StackTraceElement> {
|
||||||
|
val frameList = context.invokeMethod(info, refs.lastObservedStackTrace, emptyList()) as ObjectReference
|
||||||
|
val mergedFrameList = context.invokeMethod(
|
||||||
|
refs.debugProbesImpl,
|
||||||
|
refs.enhanceStackTraceWithThreadDump, listOf(info, frameList)
|
||||||
|
) as ObjectReference
|
||||||
|
val size = (context.invokeMethod(mergedFrameList, refs.getSize, emptyList()) as IntegerValue).value()
|
||||||
|
|
||||||
|
val list = ArrayList<StackTraceElement>()
|
||||||
|
for (it in size - 1 downTo 0) {
|
||||||
|
val frame = context.invokeMethod(
|
||||||
|
mergedFrameList, refs.getElement,
|
||||||
|
listOf(context.vm.virtualMachine.mirrorOf(it))
|
||||||
|
) as ObjectReference
|
||||||
|
val clazz = (frame.getValue(refs.className) as StringReference).value()
|
||||||
|
|
||||||
|
// if (clazz.contains("$DEBUG_PACKAGE.DebugProbes")) break // cut off debug intrinsic stacktrace
|
||||||
|
list.add(
|
||||||
|
0, // add in the beginning
|
||||||
|
StackTraceElement(
|
||||||
|
clazz,
|
||||||
|
(frame.getValue(refs.methodName) as StringReference).value(),
|
||||||
|
(frame.getValue(refs.fileName) as StringReference?)?.value(),
|
||||||
|
(frame.getValue(refs.line) as IntegerValue).value()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds ClassTypes, Methods, ObjectReferences and Fields for a particular jvm
|
||||||
|
*/
|
||||||
|
private class ProcessReferences(context: ExecutionContext) {
|
||||||
|
// kotlinx.coroutines.debug.DebugProbes instance and methods
|
||||||
|
val debugProbes = context.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType
|
||||||
|
val probesImplType = context.findClass("$DEBUG_PACKAGE.internal.DebugProbesImpl") as ClassType
|
||||||
|
val debugProbesImpl = with(probesImplType) { getValue(fieldByName("INSTANCE")) as ObjectReference }
|
||||||
|
val enhanceStackTraceWithThreadDump: Method = probesImplType
|
||||||
|
.methodsByName("enhanceStackTraceWithThreadDump").single()
|
||||||
|
|
||||||
|
val dumpMethod: Method = debugProbes.concreteMethodByName("dumpCoroutinesInfo", "()Ljava/util/List;")
|
||||||
|
val instance = with(debugProbes) { getValue(fieldByName("INSTANCE")) as ObjectReference }
|
||||||
|
|
||||||
|
// CoroutineInfo
|
||||||
|
val info = context.findClass("$DEBUG_PACKAGE.CoroutineInfo") as ClassType
|
||||||
|
val getState: Method = info.concreteMethodByName("getState", "()Lkotlinx/coroutines/debug/State;")
|
||||||
|
val getContext: Method = info.concreteMethodByName("getContext", "()Lkotlin/coroutines/CoroutineContext;")
|
||||||
|
val idField: Field = info.fieldByName("sequenceNumber")
|
||||||
|
val lastObservedStackTrace: Method = info.methodsByName("lastObservedStackTrace").single()
|
||||||
|
val coroutineContext = context.findClass("kotlin.coroutines.CoroutineContext") as InterfaceType
|
||||||
|
val getContextElement: Method = coroutineContext.methodsByName("get").single()
|
||||||
|
val coroutineName = context.findClass("kotlinx.coroutines.CoroutineName") as ClassType
|
||||||
|
val getName: Method = coroutineName.methodsByName("getName").single()
|
||||||
|
val nameKey = coroutineName.getValue(coroutineName.fieldByName("Key")) as ObjectReference
|
||||||
|
val toString: Method = (context.findClass("java.lang.Object") as ClassType)
|
||||||
|
.methodsByName("toString").single()
|
||||||
|
|
||||||
|
val threadRef: Field = info.fieldByName("lastObservedThread")
|
||||||
|
val continuation: Field = info.fieldByName("lastObservedFrame")
|
||||||
|
|
||||||
|
// Methods for list
|
||||||
|
val listType = context.findClass("java.util.List") as InterfaceType
|
||||||
|
val getSize: Method = listType.methodsByName("size").single()
|
||||||
|
val getElement: Method = listType.methodsByName("get").single()
|
||||||
|
val element = context.findClass("java.lang.StackTraceElement") as ClassType
|
||||||
|
|
||||||
|
// for StackTraceElement
|
||||||
|
val methodName: Field = element.fieldByName("methodName")
|
||||||
|
val className: Field = element.fieldByName("declaringClass")
|
||||||
|
val fileName: Field = element.fieldByName("fileName")
|
||||||
|
val line: Field = element.fieldByName("lineNumber")
|
||||||
|
}
|
||||||
|
}
|
||||||
+444
@@ -0,0 +1,444 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||||
|
|
||||||
|
import com.intellij.debugger.DebuggerBundle
|
||||||
|
import com.intellij.debugger.DebuggerInvocationUtil
|
||||||
|
import com.intellij.debugger.DebuggerManagerEx
|
||||||
|
import com.intellij.debugger.actions.GotoFrameSourceAction
|
||||||
|
import com.intellij.debugger.engine.*
|
||||||
|
import com.intellij.debugger.engine.evaluation.EvaluateException
|
||||||
|
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
|
||||||
|
import com.intellij.debugger.engine.events.DebuggerCommandImpl
|
||||||
|
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
|
||||||
|
import com.intellij.debugger.impl.DebuggerContextImpl
|
||||||
|
import com.intellij.debugger.impl.DebuggerSession
|
||||||
|
import com.intellij.debugger.jdi.StackFrameProxyImpl
|
||||||
|
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl
|
||||||
|
import com.intellij.debugger.memory.utils.StackFrameItem
|
||||||
|
import com.intellij.debugger.ui.impl.tree.TreeBuilder
|
||||||
|
import com.intellij.debugger.ui.impl.tree.TreeBuilderNode
|
||||||
|
import com.intellij.debugger.ui.impl.watch.*
|
||||||
|
import com.intellij.debugger.ui.tree.StackFrameDescriptor
|
||||||
|
import com.intellij.ide.DataManager
|
||||||
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
|
import com.intellij.openapi.application.ModalityState
|
||||||
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.openapi.ui.MessageType
|
||||||
|
import com.intellij.psi.JavaPsiFacade
|
||||||
|
import com.intellij.psi.search.GlobalSearchScope
|
||||||
|
import com.intellij.ui.DoubleClickListener
|
||||||
|
import com.intellij.xdebugger.XDebuggerUtil
|
||||||
|
import com.intellij.xdebugger.XSourcePosition
|
||||||
|
import com.intellij.xdebugger.frame.XExecutionStack
|
||||||
|
import com.intellij.xdebugger.impl.XDebuggerManagerImpl
|
||||||
|
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
|
||||||
|
import com.sun.jdi.ClassType
|
||||||
|
import javaslang.control.Either
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.KotlinCoroutinesAsyncStackTraceProvider
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.evaluate.createExecutionContext
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import javax.swing.event.TreeModelEvent
|
||||||
|
import javax.swing.event.TreeModelListener
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tree of coroutines for [CoroutinesPanel]
|
||||||
|
*/
|
||||||
|
class CoroutinesDebuggerTree(project: Project) : DebuggerTree(project) {
|
||||||
|
private val logger = Logger.getInstance(this::class.java)
|
||||||
|
private var lastSuspendContextDump: Pair<WeakReference<SuspendContextImpl>, Either<Throwable, List<CoroutineState>>>? = null
|
||||||
|
|
||||||
|
override fun createNodeManager(project: Project): NodeManagerImpl {
|
||||||
|
return object : NodeManagerImpl(project, this) {
|
||||||
|
override fun getContextKey(frame: StackFrameProxyImpl?): String? {
|
||||||
|
return "CoroutinesView"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare specific behavior instead of DebuggerTree constructor
|
||||||
|
*/
|
||||||
|
init {
|
||||||
|
val model = object : TreeBuilder(this) {
|
||||||
|
override fun buildChildren(node: TreeBuilderNode) {
|
||||||
|
val debuggerTreeNode = node as DebuggerTreeNodeImpl
|
||||||
|
if (debuggerTreeNode.descriptor is DefaultNodeDescriptor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node.add(myNodeManager.createMessageNode(MessageDescriptor.EVALUATING))
|
||||||
|
buildNode(debuggerTreeNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isExpandable(builderNode: TreeBuilderNode): Boolean {
|
||||||
|
return this@CoroutinesDebuggerTree.isExpandable(builderNode as DebuggerTreeNodeImpl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.setRoot(nodeFactory.defaultNode)
|
||||||
|
model.addTreeModelListener(createListener())
|
||||||
|
|
||||||
|
setModel(model)
|
||||||
|
emptyText.text = "Coroutines are not available"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add frames inside coroutine (node)
|
||||||
|
*/
|
||||||
|
private fun buildNode(node: DebuggerTreeNodeImpl) {
|
||||||
|
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||||
|
val debugProcess = context.debugProcess
|
||||||
|
debugProcess?.managerThread?.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
|
||||||
|
override fun contextAction(suspendContext: SuspendContextImpl) {
|
||||||
|
val evalContext = debuggerContext.createEvaluationContext() ?: return
|
||||||
|
if (node.descriptor is CoroutineDescriptorImpl || node.descriptor is CreationFramesDescriptor) {
|
||||||
|
val children = mutableListOf<DebuggerTreeNodeImpl>()
|
||||||
|
try {
|
||||||
|
addChildren(children, debugProcess, node.descriptor, evalContext)
|
||||||
|
} catch (e: EvaluateException) {
|
||||||
|
children.clear()
|
||||||
|
children.add(myNodeManager.createMessageNode(e.message))
|
||||||
|
logger.debug(e)
|
||||||
|
}
|
||||||
|
DebuggerInvocationUtil.swingInvokeLater(project) {
|
||||||
|
node.removeAllChildren()
|
||||||
|
for (debuggerTreeNode in children) {
|
||||||
|
node.add(debuggerTreeNode)
|
||||||
|
}
|
||||||
|
node.childrenChanged(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installAction(): () -> Unit {
|
||||||
|
val listener = object : DoubleClickListener() {
|
||||||
|
override fun onDoubleClick(e: MouseEvent): Boolean {
|
||||||
|
val location = getPathForLocation(e.x, e.y)
|
||||||
|
?.lastPathComponent as? DebuggerTreeNodeImpl ?: return false
|
||||||
|
return selectFrame(location.userObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listener.installOn(this)
|
||||||
|
|
||||||
|
return { listener.uninstall(this) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectFrame(descriptor: Any): Boolean {
|
||||||
|
val dataContext = DataManager.getInstance().getDataContext(this@CoroutinesDebuggerTree)
|
||||||
|
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||||
|
when (descriptor) {
|
||||||
|
is SuspendStackFrameDescriptor -> {
|
||||||
|
buildSuspendStackFrameChildren(descriptor)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
is AsyncStackFrameDescriptor -> {
|
||||||
|
buildAsyncStackFrameChildren(descriptor, context.debugProcess ?: return false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
is EmptyStackFrameDescriptor -> {
|
||||||
|
buildEmptyStackFrameChildren(descriptor)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
is StackFrameDescriptor -> {
|
||||||
|
GotoFrameSourceAction.doAction(dataContext)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else -> return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSuspendStackFrameChildren(descriptor: SuspendStackFrameDescriptor) {
|
||||||
|
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||||
|
val pos = getPosition(descriptor.frame) ?: return
|
||||||
|
context.debugProcess?.managerThread?.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
|
||||||
|
override fun contextAction() {
|
||||||
|
val (stack, stackFrame) = createSyntheticStackFrame(descriptor, pos) ?: return
|
||||||
|
val action: () -> Unit = { context.debuggerSession?.xDebugSession?.setCurrentStackFrame(stack, stackFrame) }
|
||||||
|
ApplicationManager.getApplication()
|
||||||
|
.invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAsyncStackFrameChildren(descriptor: AsyncStackFrameDescriptor, process: DebugProcessImpl) {
|
||||||
|
process.managerThread?.schedule(object : DebuggerCommandImpl() {
|
||||||
|
override fun action() {
|
||||||
|
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||||
|
val proxy = ThreadReferenceProxyImpl(
|
||||||
|
process.virtualMachineProxy,
|
||||||
|
descriptor.state.thread // is not null because it's a running coroutine
|
||||||
|
)
|
||||||
|
val executionStack = JavaExecutionStack(proxy, process, false)
|
||||||
|
executionStack.initTopFrame()
|
||||||
|
val frame = descriptor.frame.createFrame(process)
|
||||||
|
DebuggerUIUtil.invokeLater {
|
||||||
|
context.debuggerSession?.xDebugSession?.setCurrentStackFrame(
|
||||||
|
executionStack,
|
||||||
|
frame
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildEmptyStackFrameChildren(descriptor: EmptyStackFrameDescriptor) {
|
||||||
|
val position = getPosition(descriptor.frame) ?: return
|
||||||
|
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||||
|
val suspendContext = context.suspendContext ?: return
|
||||||
|
val proxy = suspendContext.thread ?: return
|
||||||
|
context.debugProcess?.managerThread?.schedule(object : DebuggerCommandImpl() {
|
||||||
|
override fun action() {
|
||||||
|
val executionStack =
|
||||||
|
JavaExecutionStack(proxy, context.debugProcess!!, false)
|
||||||
|
executionStack.initTopFrame()
|
||||||
|
val frame = SyntheticStackFrame(descriptor, emptyList(), position)
|
||||||
|
val action: () -> Unit =
|
||||||
|
{ context.debuggerSession?.xDebugSession?.setCurrentStackFrame(executionStack, frame) }
|
||||||
|
ApplicationManager.getApplication()
|
||||||
|
.invokeLater(action, ModalityState.stateForComponent(this@CoroutinesDebuggerTree))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPosition(frame: StackTraceElement): XSourcePosition? {
|
||||||
|
|
||||||
|
val psiFacade = JavaPsiFacade.getInstance(project)
|
||||||
|
val psiClass = psiFacade.findClass(
|
||||||
|
frame.className.substringBefore("$"), // find outer class, for which psi exists TODO
|
||||||
|
GlobalSearchScope.everythingScope(project)
|
||||||
|
)
|
||||||
|
val classFile = psiClass?.containingFile?.virtualFile
|
||||||
|
return XDebuggerUtil.getInstance().createPosition(classFile, frame.lineNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be invoked on manager thread
|
||||||
|
*/
|
||||||
|
private fun createSyntheticStackFrame(
|
||||||
|
descriptor: SuspendStackFrameDescriptor,
|
||||||
|
pos: XSourcePosition
|
||||||
|
): Pair<XExecutionStack, SyntheticStackFrame>? {
|
||||||
|
val context = DebuggerManagerEx.getInstanceEx(project).context
|
||||||
|
val proxy = context.suspendContext?.thread ?: return null
|
||||||
|
val executionStack =
|
||||||
|
JavaExecutionStack(proxy, context.debugProcess!!, false)
|
||||||
|
executionStack.initTopFrame()
|
||||||
|
val execContext = context.createExecutionContext() ?: return null
|
||||||
|
val continuation = descriptor.continuation // guaranteed that it is a BaseContinuationImpl
|
||||||
|
val aMethod = (continuation.type() as ClassType).concreteMethodByName(
|
||||||
|
"getStackTraceElement",
|
||||||
|
"()Ljava/lang/StackTraceElement;"
|
||||||
|
)
|
||||||
|
val debugMetadataKtType = execContext
|
||||||
|
.findClass("kotlin.coroutines.jvm.internal.DebugMetadataKt") as ClassType
|
||||||
|
val vars = with(KotlinCoroutinesAsyncStackTraceProvider()) {
|
||||||
|
KotlinCoroutinesAsyncStackTraceProvider.AsyncStackTraceContext(
|
||||||
|
execContext,
|
||||||
|
aMethod,
|
||||||
|
debugMetadataKtType
|
||||||
|
).getSpilledVariables(continuation)
|
||||||
|
} ?: return null
|
||||||
|
return executionStack to SyntheticStackFrame(descriptor, vars, pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addChildren(
|
||||||
|
children: MutableList<DebuggerTreeNodeImpl>,
|
||||||
|
debugProcess: DebugProcessImpl,
|
||||||
|
descriptor: NodeDescriptorImpl,
|
||||||
|
evalContext: EvaluationContextImpl
|
||||||
|
) {
|
||||||
|
if (descriptor !is CoroutineDescriptorImpl) {
|
||||||
|
if (descriptor is CreationFramesDescriptor) {
|
||||||
|
val threadProxy = debuggerContext.suspendContext?.thread ?: return
|
||||||
|
val proxy = threadProxy.forceFrames().first()
|
||||||
|
descriptor.frames.forEach {
|
||||||
|
children.add(myNodeManager.createNode(EmptyStackFrameDescriptor(it, proxy), evalContext))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
when (descriptor.state.state) {
|
||||||
|
CoroutineState.State.RUNNING -> {
|
||||||
|
if (descriptor.state.thread == null) {
|
||||||
|
children.add(myNodeManager.createMessageNode("Frames are not available"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val proxy = ThreadReferenceProxyImpl(
|
||||||
|
debugProcess.virtualMachineProxy,
|
||||||
|
descriptor.state.thread
|
||||||
|
)
|
||||||
|
val frames = proxy.forceFrames()
|
||||||
|
var i = frames.lastIndex
|
||||||
|
while (i > 0 && frames[i].location().method().name() != "resumeWith") i--
|
||||||
|
// if i is less than 0, wait, what?
|
||||||
|
for (frame in 0..--i) {
|
||||||
|
children.add(createFrameDescriptor(descriptor, evalContext, frames[frame]))
|
||||||
|
}
|
||||||
|
if (i > 0) { // add async stack trace if there are frames after invokeSuspend
|
||||||
|
val async = KotlinCoroutinesAsyncStackTraceProvider().getAsyncStackTrace(
|
||||||
|
JavaStackFrame(StackFrameDescriptorImpl(frames[i - 1], MethodsTracker()), true),
|
||||||
|
evalContext.suspendContext
|
||||||
|
)
|
||||||
|
async?.forEach { children.add(createAsyncFrameDescriptor(descriptor, evalContext, it, frames[0])) }
|
||||||
|
}
|
||||||
|
for (frame in i + 2..frames.lastIndex) {
|
||||||
|
children.add(createFrameDescriptor(descriptor, evalContext, frames[frame]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CoroutineState.State.SUSPENDED -> {
|
||||||
|
val threadProxy = debuggerContext.suspendContext?.thread ?: return
|
||||||
|
val proxy = threadProxy.forceFrames().first()
|
||||||
|
// the thread is paused on breakpoint - it has at least one frame
|
||||||
|
for (it in descriptor.state.stackTrace) {
|
||||||
|
if (it.className.startsWith("\b\b\b")) break
|
||||||
|
children.add(createCoroutineFrameDescriptor(descriptor, evalContext, it, proxy))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val trace = descriptor.state.stackTrace
|
||||||
|
val index = trace.indexOfFirst { it.className.startsWith("\b\b\b") }
|
||||||
|
children.add(myNodeManager.createNode(CreationFramesDescriptor(trace.subList(index + 1, trace.size)), evalContext))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun createFrameDescriptor(
|
||||||
|
descriptor: NodeDescriptorImpl,
|
||||||
|
evalContext: EvaluationContextImpl,
|
||||||
|
frame: StackFrameProxyImpl
|
||||||
|
): DebuggerTreeNodeImpl {
|
||||||
|
return myNodeManager.createNode(
|
||||||
|
myNodeManager.getStackFrameDescriptor(descriptor, frame),
|
||||||
|
evalContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCoroutineFrameDescriptor(
|
||||||
|
descriptor: CoroutineDescriptorImpl,
|
||||||
|
evalContext: EvaluationContextImpl,
|
||||||
|
frame: StackTraceElement,
|
||||||
|
proxy: StackFrameProxyImpl,
|
||||||
|
parent: NodeDescriptorImpl? = null
|
||||||
|
): DebuggerTreeNodeImpl {
|
||||||
|
return myNodeManager.createNode(
|
||||||
|
myNodeManager.getDescriptor(
|
||||||
|
parent,
|
||||||
|
CoroutineStackFrameData(descriptor.state, frame, proxy)
|
||||||
|
), evalContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAsyncFrameDescriptor(
|
||||||
|
descriptor: CoroutineDescriptorImpl,
|
||||||
|
evalContext: EvaluationContextImpl,
|
||||||
|
frame: StackFrameItem,
|
||||||
|
proxy: StackFrameProxyImpl
|
||||||
|
): DebuggerTreeNodeImpl {
|
||||||
|
return myNodeManager.createNode(
|
||||||
|
myNodeManager.getDescriptor(
|
||||||
|
descriptor,
|
||||||
|
CoroutineStackFrameData(descriptor.state, frame, proxy)
|
||||||
|
), evalContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createListener() = object : TreeModelListener {
|
||||||
|
override fun treeNodesChanged(event: TreeModelEvent) {
|
||||||
|
hideTooltip()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun treeNodesInserted(event: TreeModelEvent) {
|
||||||
|
hideTooltip()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun treeNodesRemoved(event: TreeModelEvent) {
|
||||||
|
hideTooltip()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun treeStructureChanged(event: TreeModelEvent) {
|
||||||
|
hideTooltip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isExpandable(node: DebuggerTreeNodeImpl): Boolean {
|
||||||
|
val descriptor = node.descriptor
|
||||||
|
return if (descriptor is StackFrameDescriptor) return false else descriptor.isExpandable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun build(context: DebuggerContextImpl) {
|
||||||
|
val session = context.debuggerSession
|
||||||
|
val command = RefreshCoroutinesTreeCommand(session, context.suspendContext)
|
||||||
|
|
||||||
|
val state = if (session != null) session.state else DebuggerSession.State.DISPOSED
|
||||||
|
if (ApplicationManager.getApplication().isUnitTestMode
|
||||||
|
|| state == DebuggerSession.State.PAUSED
|
||||||
|
) {
|
||||||
|
showMessage(MessageDescriptor.EVALUATING)
|
||||||
|
context.debugProcess!!.managerThread.schedule(command)
|
||||||
|
} else {
|
||||||
|
showMessage(if (session != null) session.stateDescription else DebuggerBundle.message("status.debug.stopped"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class RefreshCoroutinesTreeCommand(private val mySession: DebuggerSession?, context: SuspendContextImpl?) :
|
||||||
|
SuspendContextCommandImpl(context) {
|
||||||
|
|
||||||
|
override fun contextAction() {
|
||||||
|
val root = nodeFactory.defaultNode
|
||||||
|
mySession ?: return
|
||||||
|
val suspendContext = suspendContext
|
||||||
|
if (suspendContext == null || suspendContext.isResumed) {
|
||||||
|
setRoot(root.apply { add(myNodeManager.createMessageNode("Application is resumed")) })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val evaluationContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy)
|
||||||
|
val executionContext = ExecutionContext(evaluationContext, suspendContext.frameProxy ?: return)
|
||||||
|
val cache = lastSuspendContextDump
|
||||||
|
val states = if (cache != null && cache.first.get() === suspendContext) {
|
||||||
|
cache.second
|
||||||
|
} else CoroutinesDebugProbesProxy.dumpCoroutines(executionContext).apply {
|
||||||
|
lastSuspendContextDump = WeakReference(suspendContext) to this
|
||||||
|
}
|
||||||
|
// if suspend context hasn't changed - use last dump, else compute new
|
||||||
|
if (states.isLeft) {
|
||||||
|
logger.warn(states.left)
|
||||||
|
setRoot(root.apply {
|
||||||
|
clear()
|
||||||
|
add(nodeFactory.createMessageNode(MessageDescriptor("Dump failed")))
|
||||||
|
})
|
||||||
|
XDebuggerManagerImpl.NOTIFICATION_GROUP
|
||||||
|
.createNotification(
|
||||||
|
"Coroutine dump failed. See log",
|
||||||
|
MessageType.ERROR
|
||||||
|
).notify(project)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (state in states.get()) {
|
||||||
|
root.add(
|
||||||
|
nodeFactory.createNode(
|
||||||
|
nodeFactory.getDescriptor(null, CoroutineData(state)), evaluationContext
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setRoot(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setRoot(root: DebuggerTreeNodeImpl) {
|
||||||
|
DebuggerInvocationUtil.swingInvokeLater(project) {
|
||||||
|
mutableModel.setRoot(root)
|
||||||
|
treeChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||||
|
|
||||||
|
import com.intellij.debugger.engine.events.SuspendContextCommandImpl
|
||||||
|
import com.intellij.debugger.impl.DebuggerContextImpl
|
||||||
|
import com.intellij.debugger.impl.DebuggerContextListener
|
||||||
|
import com.intellij.debugger.impl.DebuggerSession
|
||||||
|
import com.intellij.debugger.impl.DebuggerStateManager
|
||||||
|
import com.intellij.debugger.ui.impl.DebuggerTreePanel
|
||||||
|
import com.intellij.debugger.ui.impl.watch.DebuggerTree
|
||||||
|
import com.intellij.debugger.ui.impl.watch.DebuggerTreeNodeImpl
|
||||||
|
import com.intellij.openapi.actionSystem.ActionManager
|
||||||
|
import com.intellij.openapi.actionSystem.ActionPopupMenu
|
||||||
|
import com.intellij.openapi.actionSystem.EmptyActionGroup
|
||||||
|
import com.intellij.openapi.actionSystem.PlatformDataKeys
|
||||||
|
import com.intellij.openapi.application.ModalityState
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.openapi.util.Disposer
|
||||||
|
import com.intellij.ui.ScrollPaneFactory
|
||||||
|
import com.intellij.util.Alarm
|
||||||
|
import org.jetbrains.annotations.NonNls
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
import java.awt.event.KeyAdapter
|
||||||
|
import java.awt.event.KeyEvent
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually added into ui in [CoroutinesDebugConfigurationExtension.registerCoroutinesPanel]
|
||||||
|
* Some methods are copied from [com.intellij.debugger.ui.impl.ThreadsPanel]
|
||||||
|
*/
|
||||||
|
class CoroutinesPanel(project: Project, stateManager: DebuggerStateManager) : DebuggerTreePanel(project, stateManager) {
|
||||||
|
private val myUpdateLabelsAlarm = Alarm(Alarm.ThreadToUse.SWING_THREAD)
|
||||||
|
|
||||||
|
init {
|
||||||
|
val disposable = getCoroutinesTree().installAction()
|
||||||
|
registerDisposable(disposable)
|
||||||
|
getCoroutinesTree().addKeyListener(object : KeyAdapter() {
|
||||||
|
override fun keyPressed(e: KeyEvent) {
|
||||||
|
if (e.keyCode == KeyEvent.VK_ENTER && getCoroutinesTree().selectionCount == 1) {
|
||||||
|
val selected = getCoroutinesTree().selectionModel.selectionPath.lastPathComponent
|
||||||
|
if (selected is DebuggerTreeNodeImpl) getCoroutinesTree().selectFrame(selected.userObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
add(ScrollPaneFactory.createScrollPane(getCoroutinesTree()), BorderLayout.CENTER)
|
||||||
|
stateManager.addListener(object : DebuggerContextListener {
|
||||||
|
override fun changeEvent(newContext: DebuggerContextImpl, event: DebuggerSession.Event) {
|
||||||
|
if (DebuggerSession.Event.ATTACHED == event || DebuggerSession.Event.RESUME == event) {
|
||||||
|
startLabelsUpdate()
|
||||||
|
} else if (DebuggerSession.Event.PAUSE == event
|
||||||
|
|| DebuggerSession.Event.DETACHED == event
|
||||||
|
|| DebuggerSession.Event.DISPOSE == event
|
||||||
|
) {
|
||||||
|
myUpdateLabelsAlarm.cancelAllRequests()
|
||||||
|
}
|
||||||
|
if (DebuggerSession.Event.DETACHED == event || DebuggerSession.Event.DISPOSE == event) {
|
||||||
|
stateManager.removeListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
startLabelsUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startLabelsUpdate() {
|
||||||
|
if (myUpdateLabelsAlarm.isDisposed) return
|
||||||
|
myUpdateLabelsAlarm.cancelAllRequests()
|
||||||
|
myUpdateLabelsAlarm.addRequest(object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
var updateScheduled = false
|
||||||
|
try {
|
||||||
|
if (isUpdateEnabled) {
|
||||||
|
val tree = getCoroutinesTree()
|
||||||
|
val root = tree.model.root as DebuggerTreeNodeImpl
|
||||||
|
val process = context.debugProcess
|
||||||
|
if (process != null) {
|
||||||
|
process.managerThread.schedule(object : SuspendContextCommandImpl(context.suspendContext) {
|
||||||
|
override fun contextAction() {
|
||||||
|
try {
|
||||||
|
updateNodeLabels(root)
|
||||||
|
} finally {
|
||||||
|
reschedule()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun commandCancelled() {
|
||||||
|
reschedule()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
updateScheduled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!updateScheduled) {
|
||||||
|
reschedule()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reschedule() {
|
||||||
|
val session = context.debuggerSession
|
||||||
|
if (session != null && session.isAttached && !session.isPaused && !myUpdateLabelsAlarm.isDisposed) {
|
||||||
|
myUpdateLabelsAlarm.addRequest(
|
||||||
|
this,
|
||||||
|
LABELS_UPDATE_DELAY_MS, ModalityState.NON_MODAL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, LABELS_UPDATE_DELAY_MS, ModalityState.NON_MODAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
Disposer.dispose(myUpdateLabelsAlarm)
|
||||||
|
super.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNodeLabels(from: DebuggerTreeNodeImpl) {
|
||||||
|
val children = from.children()
|
||||||
|
try {
|
||||||
|
while (children.hasMoreElements()) {
|
||||||
|
val child = children.nextElement() as DebuggerTreeNodeImpl
|
||||||
|
child.descriptor.updateRepresentation(
|
||||||
|
null
|
||||||
|
) { child.labelChanged() }
|
||||||
|
updateNodeLabels(child)
|
||||||
|
}
|
||||||
|
} catch (ignored: NoSuchElementException) { // children have changed - just skip
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createTreeView(): DebuggerTree {
|
||||||
|
return CoroutinesDebuggerTree(project)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPopupMenu(): ActionPopupMenu {
|
||||||
|
val group = EmptyActionGroup()
|
||||||
|
return ActionManager.getInstance().createActionPopupMenu("Debugger.CoroutinesPanelPopup", group)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getData(dataId: String): Any? {
|
||||||
|
return if (PlatformDataKeys.HELP_ID.`is`(dataId)) {
|
||||||
|
HELP_ID
|
||||||
|
} else super.getData(dataId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCoroutinesTree(): CoroutinesDebuggerTree = tree as CoroutinesDebuggerTree
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@NonNls
|
||||||
|
private val HELP_ID = "debugging.debugCoroutines"
|
||||||
|
private const val LABELS_UPDATE_DELAY_MS = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+222
@@ -0,0 +1,222 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.idea.debugger.coroutines
|
||||||
|
|
||||||
|
import com.intellij.debugger.DebuggerManagerEx
|
||||||
|
import com.intellij.debugger.engine.SuspendContextImpl
|
||||||
|
import com.intellij.debugger.engine.evaluation.EvaluateException
|
||||||
|
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
|
||||||
|
import com.intellij.debugger.impl.DebuggerUtilsEx
|
||||||
|
import com.intellij.debugger.impl.descriptors.data.DescriptorData
|
||||||
|
import com.intellij.debugger.impl.descriptors.data.DisplayKey
|
||||||
|
import com.intellij.debugger.impl.descriptors.data.SimpleDisplayKey
|
||||||
|
import com.intellij.debugger.jdi.StackFrameProxyImpl
|
||||||
|
import com.intellij.debugger.memory.utils.StackFrameItem
|
||||||
|
import com.intellij.debugger.ui.impl.watch.MethodsTracker
|
||||||
|
import com.intellij.debugger.ui.impl.watch.NodeDescriptorImpl
|
||||||
|
import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl
|
||||||
|
import com.intellij.debugger.ui.tree.render.DescriptorLabelListener
|
||||||
|
import com.intellij.icons.AllIcons
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.openapi.util.IconLoader
|
||||||
|
import com.sun.jdi.ClassType
|
||||||
|
import com.sun.jdi.ObjectReference
|
||||||
|
import org.jetbrains.kotlin.idea.IconExtensionChooser
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||||
|
import javax.swing.Icon
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes coroutine itself in the tree (name: STATE), has children if stacktrace is not empty (state = CREATED)
|
||||||
|
*/
|
||||||
|
class CoroutineData(private val state: CoroutineState) : DescriptorData<CoroutineDescriptorImpl>() {
|
||||||
|
|
||||||
|
override fun createDescriptorImpl(project: Project): CoroutineDescriptorImpl {
|
||||||
|
return CoroutineDescriptorImpl(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return if (other !is CoroutineData) {
|
||||||
|
false
|
||||||
|
} else state.name == other.state.name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return state.name.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDisplayKey(): DisplayKey<CoroutineDescriptorImpl> {
|
||||||
|
return SimpleDisplayKey(state.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CoroutineDescriptorImpl(val state: CoroutineState) : NodeDescriptorImpl() {
|
||||||
|
var suspendContext: SuspendContextImpl? = null
|
||||||
|
val icon: Icon
|
||||||
|
get() = when {
|
||||||
|
state.isSuspended -> AllIcons.Debugger.ThreadSuspended
|
||||||
|
state.state == CoroutineState.State.CREATED -> AllIcons.Debugger.ThreadStates.Idle
|
||||||
|
else -> AllIcons.Debugger.ThreadRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String? {
|
||||||
|
return state.name
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(EvaluateException::class)
|
||||||
|
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener): String {
|
||||||
|
val name = if (state.thread != null) state.thread.name().substringBefore(" @${state.name}") else ""
|
||||||
|
val threadState = if (state.thread != null) DebuggerUtilsEx.getThreadStatusText(state.thread.status()) else ""
|
||||||
|
return "${state.name}: ${state.state}${if (name.isNotEmpty()) " on thread \"$name\":$threadState" else ""}"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isExpandable(): Boolean {
|
||||||
|
return state.state != CoroutineState.State.CREATED
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setContext(context: EvaluationContextImpl?) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CoroutineStackFrameData private constructor(val state: CoroutineState, private val proxy: StackFrameProxyImpl) :
|
||||||
|
DescriptorData<NodeDescriptorImpl>() {
|
||||||
|
private var frame: StackTraceElement? = null
|
||||||
|
private var frameItem: StackFrameItem? = null
|
||||||
|
|
||||||
|
constructor(state: CoroutineState, frame: StackTraceElement, proxy: StackFrameProxyImpl) : this(state, proxy) {
|
||||||
|
this.frame = frame
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(state: CoroutineState, frameItem: StackFrameItem, proxy: StackFrameProxyImpl) : this(state, proxy) {
|
||||||
|
this.frameItem = frameItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode() = frame?.hashCode() ?: frameItem.hashCode()
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return if (other is CoroutineStackFrameData) {
|
||||||
|
other.frame == frame && other.frameItem == frameItem
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [EmptyStackFrameDescriptor], [SuspendStackFrameDescriptor]
|
||||||
|
* or [AsyncStackFrameDescriptor] according to current frame
|
||||||
|
*/
|
||||||
|
override fun createDescriptorImpl(project: Project): NodeDescriptorImpl {
|
||||||
|
val frame = frame ?: return AsyncStackFrameDescriptor(
|
||||||
|
state,
|
||||||
|
frameItem!!,
|
||||||
|
proxy
|
||||||
|
)
|
||||||
|
// check whether last fun is suspend fun
|
||||||
|
val suspendContext =
|
||||||
|
DebuggerManagerEx.getInstanceEx(project).context.suspendContext ?: return EmptyStackFrameDescriptor(
|
||||||
|
frame,
|
||||||
|
proxy
|
||||||
|
)
|
||||||
|
val suspendProxy = suspendContext.frameProxy ?: return EmptyStackFrameDescriptor(
|
||||||
|
frame,
|
||||||
|
proxy
|
||||||
|
)
|
||||||
|
val evalContext = EvaluationContextImpl(suspendContext, suspendContext.frameProxy)
|
||||||
|
val context = ExecutionContext(evalContext, suspendProxy)
|
||||||
|
val clazz = context.findClass(frame.className) as ClassType
|
||||||
|
val method = clazz.methodsByName(frame.methodName).last {
|
||||||
|
val loc = it.location().lineNumber()
|
||||||
|
loc < 0 && frame.lineNumber < 0 || loc > 0 && loc <= frame.lineNumber
|
||||||
|
} // pick correct method if an overloaded one is given
|
||||||
|
return if ("Lkotlin/coroutines/Continuation;)" in method.signature() ||
|
||||||
|
method.name() == "invokeSuspend" &&
|
||||||
|
method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;" // suspend fun or invokeSuspend
|
||||||
|
) {
|
||||||
|
val continuation = state.getContinuation(frame, context)
|
||||||
|
if (continuation == null) EmptyStackFrameDescriptor(
|
||||||
|
frame,
|
||||||
|
proxy
|
||||||
|
) else
|
||||||
|
SuspendStackFrameDescriptor(
|
||||||
|
state,
|
||||||
|
frame,
|
||||||
|
proxy,
|
||||||
|
continuation
|
||||||
|
)
|
||||||
|
} else EmptyStackFrameDescriptor(frame, proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDisplayKey(): DisplayKey<NodeDescriptorImpl> = SimpleDisplayKey(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descriptor for suspend functions
|
||||||
|
*/
|
||||||
|
class SuspendStackFrameDescriptor(
|
||||||
|
val state: CoroutineState,
|
||||||
|
val frame: StackTraceElement,
|
||||||
|
proxy: StackFrameProxyImpl,
|
||||||
|
val continuation: ObjectReference
|
||||||
|
) :
|
||||||
|
StackFrameDescriptorImpl(proxy, MethodsTracker()) {
|
||||||
|
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
|
||||||
|
return with(frame) {
|
||||||
|
val pack = className.substringBeforeLast(".", "")
|
||||||
|
"$methodName:$lineNumber, ${className.substringAfterLast(".")} " +
|
||||||
|
if (pack.isNotEmpty()) "{$pack}" else ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isExpandable() = false
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return frame.methodName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(): Icon {
|
||||||
|
return IconLoader.getIcon("org/jetbrains/kotlin/idea/icons/suspendCall.${IconExtensionChooser.iconExtension()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AsyncStackFrameDescriptor(val state: CoroutineState, val frame: StackFrameItem, proxy: StackFrameProxyImpl) :
|
||||||
|
StackFrameDescriptorImpl(proxy, MethodsTracker()) {
|
||||||
|
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
|
||||||
|
return with(frame) {
|
||||||
|
val pack = path().substringBeforeLast(".", "")
|
||||||
|
"${method()}:${line()}, ${path().substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return frame.method()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isExpandable(): Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For the case when no data inside frame is available
|
||||||
|
*/
|
||||||
|
class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) :
|
||||||
|
StackFrameDescriptorImpl(proxy, MethodsTracker()) {
|
||||||
|
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
|
||||||
|
return with(frame) {
|
||||||
|
val pack = className.substringBeforeLast(".", "")
|
||||||
|
"$methodName:$lineNumber, ${className.substringAfterLast(".")} ${if (pack.isNotEmpty()) "{$pack}" else ""}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName() = null
|
||||||
|
override fun isExpandable() = false
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreationFramesDescriptor(val frames: List<StackTraceElement>) : NodeDescriptorImpl() {
|
||||||
|
override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener?): String {
|
||||||
|
return "Coroutine creation stack trace"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setContext(context: EvaluationContextImpl?) {}
|
||||||
|
override fun getName() = "Coroutine creation stack trace"
|
||||||
|
override fun isExpandable() = true
|
||||||
|
}
|
||||||
+7
@@ -6,10 +6,12 @@
|
|||||||
package org.jetbrains.kotlin.idea.debugger.evaluate
|
package org.jetbrains.kotlin.idea.debugger.evaluate
|
||||||
|
|
||||||
import com.intellij.debugger.engine.DebugProcessImpl
|
import com.intellij.debugger.engine.DebugProcessImpl
|
||||||
|
import com.intellij.debugger.engine.DebuggerManagerThreadImpl
|
||||||
import com.intellij.debugger.engine.SuspendContextImpl
|
import com.intellij.debugger.engine.SuspendContextImpl
|
||||||
import com.intellij.debugger.engine.evaluation.EvaluateException
|
import com.intellij.debugger.engine.evaluation.EvaluateException
|
||||||
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
|
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
|
||||||
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
|
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
|
||||||
|
import com.intellij.debugger.impl.DebuggerContextImpl
|
||||||
import com.intellij.debugger.impl.DebuggerUtilsEx
|
import com.intellij.debugger.impl.DebuggerUtilsEx
|
||||||
import com.intellij.debugger.jdi.StackFrameProxyImpl
|
import com.intellij.debugger.jdi.StackFrameProxyImpl
|
||||||
import com.intellij.debugger.jdi.VirtualMachineProxyImpl
|
import com.intellij.debugger.jdi.VirtualMachineProxyImpl
|
||||||
@@ -98,4 +100,9 @@ class ExecutionContext(val evaluationContext: EvaluationContextImpl, val framePr
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
DebuggerUtilsEx.keep(reference, evaluationContext)
|
DebuggerUtilsEx.keep(reference, evaluationContext)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DebuggerContextImpl.createExecutionContext(): ExecutionContext? {
|
||||||
|
val context = createEvaluationContext()
|
||||||
|
return ExecutionContext(context ?: return null, frameProxy ?: return null)
|
||||||
}
|
}
|
||||||
@@ -60,6 +60,24 @@
|
|||||||
<add-to-group group-id="XDebugger.Settings" relative-to-action="XDebugger.Inline" anchor="after"/>
|
<add-to-group group-id="XDebugger.Settings" relative-to-action="XDebugger.Inline" anchor="after"/>
|
||||||
</action>
|
</action>
|
||||||
|
|
||||||
|
<group id="Kotlin.XDebugger.Actions">
|
||||||
|
<action id="Kotlin.XDebugger.ToggleKotlinVariableView"
|
||||||
|
class="org.jetbrains.kotlin.idea.debugger.ToggleKotlinVariablesView"
|
||||||
|
icon="/org/jetbrains/kotlin/idea/icons/kotlin.png"
|
||||||
|
text="Show Kotlin variables only"
|
||||||
|
/>
|
||||||
|
<!-- TODO(design)-->
|
||||||
|
<action id="Kotlin.XDebugger.CoroutinesDump"
|
||||||
|
class="org.jetbrains.kotlin.idea.debugger.coroutines.CoroutineDumpAction"
|
||||||
|
text="Get Coroutines Dump"
|
||||||
|
icon="/org/jetbrains/kotlin/idea/icons/kotlin.png"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group id="Kotlin.XDebugger.Watches.Tree.Toolbar">
|
||||||
|
<reference ref="Kotlin.XDebugger.ToggleKotlinVariableView"/>
|
||||||
|
<add-to-group group-id="XDebugger.Watches.Tree.Toolbar" relative-to-action="XDebugger.SwitchWatchesInVariables" anchor="after"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
<action id="InspectBreakpointApplicability" class="org.jetbrains.kotlin.idea.debugger.breakpoints.InspectBreakpointApplicabilityAction"
|
<action id="InspectBreakpointApplicability" class="org.jetbrains.kotlin.idea.debugger.breakpoints.InspectBreakpointApplicabilityAction"
|
||||||
text="Inspect Breakpoint Applicability" internal="true">
|
text="Inspect Breakpoint Applicability" internal="true">
|
||||||
<add-to-group group-id="KotlinInternalGroup"/>
|
<add-to-group group-id="KotlinInternalGroup"/>
|
||||||
@@ -103,6 +121,7 @@
|
|||||||
<debugger.javaBreakpointHandlerFactory implementation="org.jetbrains.kotlin.idea.debugger.breakpoints.KotlinFunctionBreakpointHandlerFactory"/>
|
<debugger.javaBreakpointHandlerFactory implementation="org.jetbrains.kotlin.idea.debugger.breakpoints.KotlinFunctionBreakpointHandlerFactory"/>
|
||||||
<debugger.jvmSteppingCommandProvider implementation="org.jetbrains.kotlin.idea.debugger.stepping.KotlinSteppingCommandProvider"/>
|
<debugger.jvmSteppingCommandProvider implementation="org.jetbrains.kotlin.idea.debugger.stepping.KotlinSteppingCommandProvider"/>
|
||||||
<debugger.simplePropertyGetterProvider implementation="org.jetbrains.kotlin.idea.debugger.stepping.KotlinSimpleGetterProvider"/>
|
<debugger.simplePropertyGetterProvider implementation="org.jetbrains.kotlin.idea.debugger.stepping.KotlinSimpleGetterProvider"/>
|
||||||
|
<runConfigurationExtension implementation="org.jetbrains.kotlin.idea.debugger.coroutines.CoroutinesDebugConfigurationExtension"/>
|
||||||
|
|
||||||
<framework.type implementation="org.jetbrains.kotlin.idea.framework.JavaFrameworkType"/>
|
<framework.type implementation="org.jetbrains.kotlin.idea.framework.JavaFrameworkType"/>
|
||||||
<projectTemplatesFactory implementation="org.jetbrains.kotlin.idea.framework.KotlinTemplatesFactory" />
|
<projectTemplatesFactory implementation="org.jetbrains.kotlin.idea.framework.KotlinTemplatesFactory" />
|
||||||
@@ -181,6 +200,10 @@
|
|||||||
description="Enable bytecode instrumentation for Kotlin classes"
|
description="Enable bytecode instrumentation for Kotlin classes"
|
||||||
defaultValue="false"
|
defaultValue="false"
|
||||||
restartRequired="false"/>
|
restartRequired="false"/>
|
||||||
|
<registryKey key="kotlin.debugger.coroutines"
|
||||||
|
description="Enable debugging for coroutines in Kotlin/JVM"
|
||||||
|
defaultValue="false"
|
||||||
|
restartRequired="false"/>
|
||||||
</extensions>
|
</extensions>
|
||||||
|
|
||||||
<extensions defaultExtensionNs="org.jetbrains.uast">
|
<extensions defaultExtensionNs="org.jetbrains.uast">
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package noCoroutines
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
//Breakpoint!
|
||||||
|
val s = "nothing to see here, folks"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
LineBreakpoint created at noCoroutines.kt:5
|
||||||
|
Run Java
|
||||||
|
Connected to the target VM
|
||||||
|
noCoroutines.kt:5
|
||||||
|
Disconnected from the target VM
|
||||||
|
|
||||||
|
Process finished with exit code 0
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package threeCoroutines
|
||||||
|
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
suspend fun main() {
|
||||||
|
sequence {
|
||||||
|
yield(239)
|
||||||
|
sequence {
|
||||||
|
//Breakpoint!
|
||||||
|
yield(666)
|
||||||
|
}.toList()
|
||||||
|
}.toList()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
LineBreakpoint created at threeCoroutines.kt:10
|
||||||
|
Run Java
|
||||||
|
Connected to the target VM
|
||||||
|
threeCoroutines.kt:10
|
||||||
|
"coroutine#1", state: RUNNING
|
||||||
|
"coroutine#2", state: RUNNING
|
||||||
|
"coroutine#3", state: RUNNING
|
||||||
|
Disconnected from the target VM
|
||||||
|
|
||||||
|
Process finished with exit code 0
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package twoDumps
|
||||||
|
|
||||||
|
suspend fun main() {
|
||||||
|
foo()
|
||||||
|
sequence<Int> {
|
||||||
|
foo()
|
||||||
|
yield(666)
|
||||||
|
}.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun foo() {
|
||||||
|
val f = 239
|
||||||
|
bar()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun bar() {
|
||||||
|
var r = 1337
|
||||||
|
//Breakpoint!
|
||||||
|
r += 42
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun SequenceScope<Int>.foo() {
|
||||||
|
val k = 228
|
||||||
|
bar()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun SequenceScope<Int>.bar() {
|
||||||
|
var r = 1337
|
||||||
|
//Breakpoint!
|
||||||
|
r += 42
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
LineBreakpoint created at twoDumps.kt:19
|
||||||
|
LineBreakpoint created at twoDumps.kt:30
|
||||||
|
Run Java
|
||||||
|
Connected to the target VM
|
||||||
|
twoDumps.kt:19
|
||||||
|
"coroutine#1", state: RUNNING
|
||||||
|
twoDumps.kt:30
|
||||||
|
"coroutine#1", state: RUNNING
|
||||||
|
"coroutine#2", state: RUNNING
|
||||||
|
Disconnected from the target VM
|
||||||
|
|
||||||
|
Process finished with exit code 0
|
||||||
@@ -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.coroutines
|
||||||
|
|
||||||
|
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
|
||||||
|
import com.intellij.execution.configurations.JavaParameters
|
||||||
|
import com.intellij.execution.process.ProcessOutputTypes
|
||||||
|
import com.intellij.jarRepository.JarRepositoryManager
|
||||||
|
import com.intellij.jarRepository.RemoteRepositoryDescription
|
||||||
|
import com.intellij.openapi.util.io.FileUtil
|
||||||
|
import org.jetbrains.idea.maven.aether.ArtifactKind
|
||||||
|
import org.jetbrains.jps.model.library.JpsMavenRepositoryLibraryDescriptor
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.KotlinDebuggerTestBase
|
||||||
|
import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
abstract class AbstractCoroutineDumpTest : KotlinDebuggerTestBase() {
|
||||||
|
|
||||||
|
|
||||||
|
protected fun doTest(path: String) {
|
||||||
|
val fileText = FileUtil.loadFile(File(path))
|
||||||
|
|
||||||
|
configureSettings(fileText)
|
||||||
|
createAdditionalBreakpoints(fileText)
|
||||||
|
createDebugProcess(path)
|
||||||
|
|
||||||
|
doOnBreakpoint {
|
||||||
|
val evalContext = EvaluationContextImpl(this, frameProxy)
|
||||||
|
val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint)
|
||||||
|
val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
|
||||||
|
try {
|
||||||
|
if (either.isRight)
|
||||||
|
try {
|
||||||
|
val states = either.get()
|
||||||
|
print(stringDump(states), ProcessOutputTypes.SYSTEM)
|
||||||
|
} catch (ignored: Throwable) {
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw AssertionError("Dump failed", either.left)
|
||||||
|
} finally {
|
||||||
|
resume(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doOnBreakpoint {
|
||||||
|
val evalContext = EvaluationContextImpl(this, frameProxy)
|
||||||
|
val execContext = ExecutionContext(evalContext, frameProxy ?: return@doOnBreakpoint)
|
||||||
|
val either = CoroutinesDebugProbesProxy.dumpCoroutines(execContext)
|
||||||
|
try {
|
||||||
|
if (either.isRight)
|
||||||
|
try {
|
||||||
|
val states = either.get()
|
||||||
|
print(stringDump(states), ProcessOutputTypes.SYSTEM)
|
||||||
|
} catch (ignored: Throwable) {
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw AssertionError("Dump failed", either.left)
|
||||||
|
} finally {
|
||||||
|
resume(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stringDump(states: List<CoroutineState>) = buildString {
|
||||||
|
states.forEach {
|
||||||
|
appendln("\"${it.name}\", state: ${it.state}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createJavaParameters(mainClass: String?): JavaParameters {
|
||||||
|
val description = JpsMavenRepositoryLibraryDescriptor("org.jetbrains.kotlinx", "kotlinx-coroutines-debug", "1.3.0")
|
||||||
|
val debugJar = JarRepositoryManager.loadDependenciesSync(
|
||||||
|
project, description, setOf(ArtifactKind.ARTIFACT),
|
||||||
|
RemoteRepositoryDescription.DEFAULT_REPOSITORIES, null
|
||||||
|
) ?: throw AssertionError("Debug Dependency is not found")
|
||||||
|
val params = super.createJavaParameters(mainClass)
|
||||||
|
for (jar in debugJar) {
|
||||||
|
params.classPath.add(jar.file.presentableUrl)
|
||||||
|
if (jar.file.name.contains("kotlinx-coroutines-debug"))
|
||||||
|
params.vmParametersList.add("-javaagent:${jar.file.presentableUrl}")
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.idea.debugger.coroutines;
|
||||||
|
|
||||||
|
import com.intellij.testFramework.TestDataPath;
|
||||||
|
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
|
||||||
|
import org.jetbrains.kotlin.test.KotlinTestUtils;
|
||||||
|
import org.jetbrains.kotlin.test.TargetBackend;
|
||||||
|
import org.jetbrains.kotlin.test.TestMetadata;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
@TestMetadata("idea/testData/debugger/tinyApp/src/coroutines")
|
||||||
|
@TestDataPath("$PROJECT_ROOT")
|
||||||
|
@RunWith(JUnit3RunnerWithInners.class)
|
||||||
|
public class CoroutineDumpTestGenerated extends AbstractCoroutineDumpTest {
|
||||||
|
private void runTest(String testDataFilePath) throws Exception {
|
||||||
|
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testAllFilesPresentInCoroutines() throws Exception {
|
||||||
|
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/debugger/tinyApp/src/coroutines"), Pattern.compile("^(.+)\\.(kt|kts)$"), TargetBackend.ANY, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("noCoroutines.kt")
|
||||||
|
public void testNoCoroutines() throws Exception {
|
||||||
|
runTest("idea/testData/debugger/tinyApp/src/coroutines/noCoroutines.kt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("threeCoroutines.kt")
|
||||||
|
public void testThreeCoroutines() throws Exception {
|
||||||
|
runTest("idea/testData/debugger/tinyApp/src/coroutines/threeCoroutines.kt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("twoDumps.kt")
|
||||||
|
public void testTwoDumps() throws Exception {
|
||||||
|
runTest("idea/testData/debugger/tinyApp/src/coroutines/twoDumps.kt");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user