diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt.193 b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt.193 new file mode 100644 index 00000000000..411f2b645ae --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugConfigurationExtension.kt.193 @@ -0,0 +1,127 @@ +/* + * 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 ?> updateJavaParameters( + configuration: T, + params: JavaParameters, + runnerSettings: RunnerSettings? + ) { + if (!Registry.`is`("kotlin.debugger.coroutines")) return + if (runnerSettings is DebuggingRunnerData + && 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 + val ui = session?.xDebugSession?.ui + if (ui != null) + registerCoroutinesPanel(ui, session) + } + } + + override fun processStopped(debugProcess: XDebugProcess) { + connection.disconnect() + project.listenerCreated = false + } + } + } + + /** + * Adds panel to XDebugSessionTab + */ + private fun registerCoroutinesPanel(ui: RunnerLayoutUi, session: DebuggerSession) { + val panel = CoroutinesPanel(session.project, session.contextManager) + val content = ui.createContent( + "CoroutinesContent", panel, "Coroutines", // TODO(design) + AllIcons.Debugger.ThreadGroup, null + ) + content.isCloseable = false + ui.addContent(content, 0, PlaceInGrid.left, true) + ui.addListener(object : ContentManagerAdapter() { + override fun selectionChanged(event: ContentManagerEvent) { + if (event.content === content) { + if (content.isSelected) { + panel.setUpdateEnabled(true) + if (panel.isRefreshNeeded) { + panel.rebuildIfVisible(DebuggerSession.Event.CONTEXT) + } + } else { + panel.setUpdateEnabled(false) + } + } + } + }, content) + // add coroutine dump button: due to api problem left toolbar is copied, modified and reset to tab + val runnerContent = (ui.options as RunnerLayoutUiImpl).getData(RunnerContentUi.KEY.name) as RunnerContentUi + val modifiedActions = runnerContent.getActions(true) + val pos = modifiedActions.indexOfLast { it is ThreadDumpAction } + modifiedActions.add(pos + 1, ActionManager.getInstance().getAction("Kotlin.XDebugger.CoroutinesDump")) + ui.options.setLeftToolbar(DefaultActionGroup(modifiedActions), ActionPlaces.DEBUGGER_TOOLBAR) + } + +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugProbesProxy.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugProbesProxy.kt index 0f6fb221003..30bc8e8a7b6 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugProbesProxy.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutinesDebugProbesProxy.kt @@ -41,13 +41,15 @@ object CoroutinesDebugProbesProxy { @Synchronized fun dumpCoroutines(context: ExecutionContext): Either> { try { - if (context.debugProcess.references == null) { - context.debugProcess.references = ProcessReferences(context) + var refs = context.debugProcess.references + if (refs == null) { + refs = ProcessReferences(context) + context.debugProcess.references = refs } - val refs = context.debugProcess.references!! // already initialized if it was null - // get dump - val infoList = context.invokeMethod(refs.instance, refs.dumpMethod, emptyList()) as ObjectReference + val infoList = context.invokeMethod(refs.instance, refs.dumpMethod, emptyList()) as? ObjectReference + ?: return Either.right(emptyList()) + context.keepReference(infoList) val size = (context.invokeMethod(infoList, refs.getSize, emptyList()) as IntegerValue).value() @@ -132,7 +134,7 @@ object CoroutinesDebugProbesProxy { mergedFrameList, refs.getElement, listOf(context.vm.virtualMachine.mirrorOf(it)) ) as ObjectReference - val clazz = (frame.getValue(refs.className) as StringReference).value() + val clazz = (frame.getValue(refs.className) as? StringReference)?.value() list.add( 0, // add in the beginning StackTraceElement( diff --git a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/descriptors.kt b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/descriptors.kt index 6d663c3e0a5..3298599bbea 100644 --- a/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/descriptors.kt +++ b/idea/jvm-debugger/jvm-debugger-core/src/org/jetbrains/kotlin/idea/debugger/coroutines/descriptors.kt @@ -164,10 +164,6 @@ class SuspendStackFrameDescriptor( 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) : diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/AbstractCoroutineDumpTest.kt b/idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/AbstractCoroutineDumpTest.kt deleted file mode 100644 index 4ef897b6fe5..00000000000 --- a/idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/AbstractCoroutineDumpTest.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -package org.jetbrains.kotlin.idea.debugger.coroutines - -import com.intellij.debugger.engine.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) = 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 - } -} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpTestGenerated.java deleted file mode 100644 index cc913593d25..00000000000 --- a/idea/tests/org/jetbrains/kotlin/idea/debugger/coroutines/CoroutineDumpTestGenerated.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -package org.jetbrains.kotlin.idea.debugger.coroutines; - -import com.intellij.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"); - } -}