From cd12226f20ae6b8555c7b2eb94ffaca19ab45b77 Mon Sep 17 00:00:00 2001 From: Vladimir Ilmov Date: Tue, 17 Mar 2020 09:40:28 +0100 Subject: [PATCH] (CoroutineDebugger) Alternative CoroutineInfoProvider added Added way to retrieve coroutine information without an agent in target JVM. 'kotlin.debugger.coroutines.switch' provides two possibilities to test coroutines. Agent way gets activated once kotlinx.coroutines.debug.DebugProbes started with javaagent. Library-less ways use DispatchedContinuation or ChildContinuationImpl classes to retrieve coroutine information and stack traces. --- ...tlinGradleCoroutineDebugProjectResolver.kt | 10 +- .../KotlinDebuggerCoroutinesBundle.properties | 3 +- .../CoroutineAsyncStackTraceProvider.kt | 82 +++--- .../CoroutineDebugConfigurationExtension.java | 37 +++ .../CoroutineDebugConfigurationExtension.kt | 30 --- ...oroutineDebugConfigurationExtension.kt.192 | 30 --- .../coroutine/CoroutineDebuggerContentInfo.kt | 7 - .../CoroutineProjectConnectionListener.kt | 117 --------- .../CoroutineStackFrameInterceptor.kt | 6 +- .../debugger/coroutine/DebuggerConnection.kt | 105 ++++++++ .../debugger/coroutine/DebuggerListener.kt | 46 ++++ .../VersionedImplementationProvider.kt | 2 +- .../coroutine/command/CoroutineBuilder.kt | 168 +++--------- .../coroutine/command/CoroutineDumpAction.kt | 9 +- .../data/ContinuationValueDescriptorImpl.kt | 31 +++ .../data/CoroutineAsyncStackFrameItem.kt | 97 ++++++- .../coroutine/data/CoroutineInfoCache.kt | 3 +- .../coroutine/data/CoroutineInfoData.kt | 44 +++- .../coroutine/data/TreeNodeDescriptorImpl.kt | 8 +- .../coroutine/proxy/AsyncStackTraceContext.kt | 174 ------------- .../coroutine/proxy/ContinuationHolder.kt | 239 ++++++++++++++++++ .../proxy/CoroutineDebugProbesProxy.kt | 218 +--------------- .../coroutine/proxy/CoroutineHolder.kt | 64 +++++ .../coroutine/proxy/CoroutineInfoProvider.kt | 12 + .../proxy/CoroutineLibraryAgentProxy.kt | 220 ++++++++++++++++ .../proxy/CoroutineNoLibraryProxy.kt | 86 +++++++ .../proxy/CoroutinePreflightStackFrame.kt | 65 +++++ .../coroutine/proxy/CoroutineUtils.kt | 133 ++++++++++ .../debugger/coroutine/proxy/FieldVariable.kt | 44 ++++ .../proxy/LocationStackFrameProxyImpl.kt | 16 ++ .../coroutine/proxy/LookupContinuation.kt | 94 ------- .../coroutine/proxy/ManagerThreadExecutor.kt | 14 +- .../proxy/mirror/coroutinesContextMirror.kt | 73 ++++++ .../proxy/mirror/coroutinesMirror.kt | 146 +++++++++++ .../coroutine/proxy/mirror/javaLangMirror.kt | 65 +++++ .../idea/debugger/coroutine/util/KUtils.kt | 6 - .../idea/debugger/coroutine/util/Providers.kt | 2 +- .../coroutine/view/CoroutineDumpPanel.kt | 19 +- .../view/CoroutineViewDebugSessionListener.kt | 1 - .../coroutine/view/SimpleColoredTextIcon.kt | 46 ++-- .../debugger/coroutine/view/XCoroutineView.kt | 225 ++++------------- .../view/XDebuggerTreeSelectedNodeListener.kt | 168 ++++++++++++ .../test/AbstractAsyncStackTraceTest.kt | 3 +- .../test/AbstractCoroutineDumpTest.kt | 2 +- .../debugger/evaluate/ExecutionContext.kt | 1 + idea/resources/META-INF/jvm.xml | 16 +- idea/resources/META-INF/plugin-common.xml | 3 + tests/mute-common.csv | 6 +- 48 files changed, 1902 insertions(+), 1094 deletions(-) create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebugConfigurationExtension.java delete mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebugConfigurationExtension.kt delete mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebugConfigurationExtension.kt.192 delete mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineProjectConnectionListener.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/DebuggerConnection.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/DebuggerListener.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/ContinuationValueDescriptorImpl.kt delete mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/AsyncStackTraceContext.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/ContinuationHolder.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineHolder.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineInfoProvider.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineLibraryAgentProxy.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineNoLibraryProxy.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutinePreflightStackFrame.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineUtils.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/FieldVariable.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/LocationStackFrameProxyImpl.kt delete mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/LookupContinuation.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/mirror/coroutinesContextMirror.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/mirror/coroutinesMirror.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/mirror/javaLangMirror.kt create mode 100644 idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/XDebuggerTreeSelectedNodeListener.kt diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleCoroutineDebugProjectResolver.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleCoroutineDebugProjectResolver.kt index 4066771220b..ccd12e148ac 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleCoroutineDebugProjectResolver.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleCoroutineDebugProjectResolver.kt @@ -6,10 +6,10 @@ package org.jetbrains.kotlin.idea.configuration import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.util.text.StringUtil import com.intellij.util.Consumer import com.intellij.util.SystemProperties +import org.jetbrains.kotlin.idea.debugger.coroutine.standaloneCoroutineDebuggerEnabled import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtension() { @@ -17,10 +17,9 @@ class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtensi override fun enhanceTaskProcessing(taskNames: MutableList, jvmParametersSetup: String?, initScriptConsumer: Consumer) { try { - if (coroutineDebuggerEnabled()) - setupCoroutineAgentForJvmForkedTestTasks(initScriptConsumer) + setupCoroutineAgentForJvmForkedTestTasks(initScriptConsumer) } catch (e: Exception) { - log.error("Gradle: not possible to attach coroutine debugger agent. Coroutine debugger disabled.", e) + log.error("Gradle: not possible to attach a coroutine debugger agent.", e) } } @@ -37,7 +36,4 @@ class KotlinGradleCoroutineDebugProjectResolver : AbstractProjectResolverExtensi val script = StringUtil.join(lines, SystemProperties.getLineSeparator()) initScriptConsumer.consume(script) } - - // supposed to be the same as [CoroutineProjectConnectionListener.kt].coroutineDebuggerEnabled - private fun coroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines") } \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/resources/messages/KotlinDebuggerCoroutinesBundle.properties b/idea/jvm-debugger/jvm-debugger-coroutine/resources/messages/KotlinDebuggerCoroutinesBundle.properties index 6479b95d0ac..973c94ce327 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/resources/messages/KotlinDebuggerCoroutinesBundle.properties +++ b/idea/jvm-debugger/jvm-debugger-coroutine/resources/messages/KotlinDebuggerCoroutinesBundle.properties @@ -22,4 +22,5 @@ coroutine.dump.threads.loading=Loading… coroutine.view.title=Coroutines coroutine.view.default.group=Default group -coroutine.view.fetching.error=An error occurred on fetching information \ No newline at end of file +coroutine.view.fetching.error=An error occurred on fetching information +coroutine.view.fetching.not_found=No coroutine information found diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineAsyncStackTraceProvider.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineAsyncStackTraceProvider.kt index 80266162aaf..a30dee49f92 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineAsyncStackTraceProvider.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineAsyncStackTraceProvider.kt @@ -5,47 +5,71 @@ package org.jetbrains.kotlin.idea.debugger.coroutine -import com.intellij.debugger.engine.* -import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.engine.AsyncStackTraceProvider +import com.intellij.debugger.engine.JavaStackFrame +import com.intellij.debugger.engine.SuspendContextImpl import com.intellij.debugger.jdi.StackFrameProxyImpl -import com.intellij.xdebugger.frame.XSuspendContext -import com.sun.jdi.* -import org.jetbrains.kotlin.idea.debugger.* +import com.intellij.debugger.memory.utils.StackFrameItem +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.* +import org.jetbrains.kotlin.idea.debugger.coroutine.data.PreCoroutineStackFrameItem import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem -import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.AsyncStackTraceContext -import org.jetbrains.kotlin.idea.debugger.evaluate.BaseExecutionContext -import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext -import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext +import org.jetbrains.kotlin.idea.debugger.hopelessAware +import org.jetbrains.kotlin.idea.debugger.isInKotlinSources class CoroutineAsyncStackTraceProvider : AsyncStackTraceProvider { - override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List? = - getAsyncStackTrace(stackFrame, suspendContext as XSuspendContext) - - fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: XSuspendContext): List? { - val stackFrameList = hopelessAware { getAsyncStackTraceSafe(stackFrame.stackFrameProxy, suspendContext) } - return if (stackFrameList == null || stackFrameList.isEmpty()) null else stackFrameList + override fun getAsyncStackTrace(stackFrame: JavaStackFrame, suspendContext: SuspendContextImpl): List? { + val stackFrameList = hopelessAware { + if (stackFrame is CoroutinePreflightStackFrame) + lookupForAfterPreflight(stackFrame, suspendContext) + else + null + } + return if (stackFrameList == null || stackFrameList.isEmpty()) + null + else stackFrameList } - fun getAsyncStackTraceSafe(frameProxy: StackFrameProxyImpl, suspendContext: XSuspendContext): List { - val defaultResult = emptyList() + fun lookupForAfterPreflight( + stackFrame: CoroutinePreflightStackFrame, + suspendContext: SuspendContextImpl + ): List? { + val resumeWithFrame = stackFrame.resumeWithFrame + if (threadAndContextSupportsEvaluation(suspendContext, resumeWithFrame)) { + val stackFrames = mutableListOf() + stackFrames.addAll( + stackFrame.restoredStackFrame.drop(1).dropLast(1) + ) // because first frame has been generated via CoroutinePreflightStackFrame + + val lastRestoredFrame = stackFrame.restoredStackFrame.last() + + stackFrames.addAll(stackFrame.threadPreCoroutineFrames.mapIndexed { index, stackFrameProxyImpl -> + if (index == 0) + PreCoroutineStackFrameItem(stackFrameProxyImpl, lastRestoredFrame) // get location and variables also from restored part + else + PreCoroutineStackFrameItem(stackFrameProxyImpl) + }) + return stackFrames + } + return null + } + + fun lookupForResumeContinuation( + frameProxy: StackFrameProxyImpl, + suspendContext: SuspendContextImpl + ): List? { val location = frameProxy.location() if (!location.isInKotlinSources()) - return defaultResult + return null - val method = location.safeMethod() ?: return defaultResult - val threadReference = frameProxy.threadProxy().threadReference - - if (threadReference == null || !threadReference.isSuspended || !canRunEvaluation(suspendContext)) - return defaultResult - - val context = DefaultExecutionContext(suspendContext as SuspendContextImpl, frameProxy) - val astContext = AsyncStackTraceContext(context, method) - return astContext.getAsyncStackTraceIfAny() + if (threadAndContextSupportsEvaluation(suspendContext, frameProxy)) + return ContinuationHolder.lookupForResumeMethodContinuation(suspendContext, frameProxy) + ?.getAsyncStackTraceIfAny()?.stackFrameItems + return null } - fun canRunEvaluation(suspendContext: XSuspendContext) = - (suspendContext as SuspendContextImpl).debugProcess.canRunEvaluation + private fun threadAndContextSupportsEvaluation(suspendContext: SuspendContextImpl, frameProxy: StackFrameProxyImpl) = + suspendContext.supportsEvaluation() && frameProxy.threadProxy().supportsEvaluation() } diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebugConfigurationExtension.java b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebugConfigurationExtension.java new file mode 100644 index 00000000000..1b300c8a361 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebugConfigurationExtension.java @@ -0,0 +1,37 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ +package org.jetbrains.kotlin.idea.debugger.coroutine; + +import com.intellij.execution.ExecutionException; +import com.intellij.execution.RunConfigurationExtension; +import com.intellij.execution.configurations.JavaParameters; +import com.intellij.execution.configurations.RunConfigurationBase; +import com.intellij.execution.configurations.RunnerSettings; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; + +public class CoroutineDebugConfigurationExtension extends RunConfigurationExtension { + private static final Logger log = Logger.getInstance(CoroutineDebugConfigurationExtension.class); + + @Override + public void updateJavaParameters( + T configuration, JavaParameters params, RunnerSettings runnerSettings + ) throws ExecutionException { + if (configuration != null) { + Project project = configuration.getProject(); + DebuggerListener listener = ServiceManager.getService(project, DebuggerListener.class); + if (listener != null) + listener.registerDebuggerConnection(configuration, params, runnerSettings); + else + log.error("DebuggerListener service is not found in project " + project.getName()); + } + } + + @Override + public boolean isApplicableFor(RunConfigurationBase configuration) { + return true; + } +} diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebugConfigurationExtension.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebugConfigurationExtension.kt deleted file mode 100644 index 15871ccf7ed..00000000000 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebugConfigurationExtension.kt +++ /dev/null @@ -1,30 +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.coroutine - -import com.intellij.execution.RunConfigurationExtension -import com.intellij.execution.configurations.DebuggingRunnerData -import com.intellij.execution.configurations.JavaParameters -import com.intellij.execution.configurations.RunConfigurationBase -import com.intellij.execution.configurations.RunnerSettings - -/** - * Installs coroutines debug agent and coroutines tab if `kotlinx-coroutines-debug` dependency is found - */ -@Suppress("IncompatibleAPI") -class CoroutineDebugConfigurationExtension : RunConfigurationExtension() { - - override fun isApplicableFor(configuration: RunConfigurationBase<*>) = coroutineDebuggerEnabled() - - override fun ?> updateJavaParameters( - configuration: T, - params: JavaParameters, - runnerSettings: RunnerSettings? - ) { - if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) { - configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings) - } - } -} diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebugConfigurationExtension.kt.192 b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebugConfigurationExtension.kt.192 deleted file mode 100644 index 5077c47f7c3..00000000000 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebugConfigurationExtension.kt.192 +++ /dev/null @@ -1,30 +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.coroutine - -import com.intellij.execution.RunConfigurationExtension -import com.intellij.execution.configurations.DebuggingRunnerData -import com.intellij.execution.configurations.JavaParameters -import com.intellij.execution.configurations.RunConfigurationBase -import com.intellij.execution.configurations.RunnerSettings - -/** - * Installs coroutines debug agent and coroutines tab if `kotlinx-coroutines-debug` dependency is found - */ -@Suppress("IncompatibleAPI") -class CoroutineDebugConfigurationExtension : RunConfigurationExtension() { - - override fun isApplicableFor(configuration: RunConfigurationBase<*>) = coroutineDebuggerEnabled() - - override fun ?> updateJavaParameters( - configuration: T, - params: JavaParameters?, - runnerSettings: RunnerSettings? - ) { - if (runnerSettings is DebuggingRunnerData && configuration is RunConfigurationBase<*>) { - configuration.project.coroutineConnectionListener.configurationStarting(configuration, params, runnerSettings) - } - } -} diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebuggerContentInfo.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebuggerContentInfo.kt index 41186fc081c..92ebfc5f850 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebuggerContentInfo.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineDebuggerContentInfo.kt @@ -13,10 +13,3 @@ class CoroutineDebuggerContentInfo { val XCOROUTINE_POPUP_ACTION_GROUP = "Kotlin.XDebugger.Actions" } } - -class CoroutineDebuggerActions { - companion object { - @NonNls - val COROUTINE_PANEL_POPUP: String = "Debugger.CoroutinesPanelPopup" - } -} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineProjectConnectionListener.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineProjectConnectionListener.kt deleted file mode 100644 index 8e45623b932..00000000000 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineProjectConnectionListener.kt +++ /dev/null @@ -1,117 +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.coroutine - -import com.intellij.debugger.DebuggerInvocationUtil -import com.intellij.debugger.engine.JavaDebugProcess -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.openapi.project.Project -import com.intellij.openapi.util.registry.Registry -import com.intellij.ui.content.Content -import com.intellij.util.messages.MessageBusConnection -import com.intellij.xdebugger.* -import com.intellij.xdebugger.impl.XDebugSessionImpl -import org.jetbrains.kotlin.idea.debugger.coroutine.util.* -import org.jetbrains.kotlin.idea.debugger.coroutine.view.XCoroutineView -import java.util.concurrent.atomic.AtomicInteger -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty - - -class CoroutineProjectConnectionListener(val project: Project) : XDebuggerManagerListener { - var connection: MessageBusConnection? = null - private val processCounter = AtomicInteger(0) - private val log by logger - - private fun connect() { - connection = project.messageBus.connect() - connection?.subscribe(XDebuggerManager.TOPIC, this) - } - - fun configurationStarting( - configuration: RunConfigurationBase<*>, - params: JavaParameters?, - runnerSettings: RunnerSettings? - ) { - if (coroutineDebuggerEnabled()) { - val configurationName = configuration.type.id - try { - if (!gradleConfiguration(configurationName)) { // gradle test logic in KotlinGradleCoroutineDebugProjectResolver - val kotlinxCoroutinesClassPathLib = - params?.classPath?.pathList?.first { it.contains("kotlinx-coroutines-debug") } - initializeCoroutineAgent(params!!, kotlinxCoroutinesClassPathLib) - } - starting() - } catch (e: NoSuchElementException) { - log.warn("'kotlinx-coroutines-debug' not found in classpath. Coroutine debugger disabled.") - } - } - } - - private fun starting() { - if (processCounter.compareAndSet(0, 1)) - connect() - else - processCounter.incrementAndGet() - } - - private fun gradleConfiguration(configurationName: String) = - "GradleRunConfiguration" == configurationName || "KotlinGradleRunConfiguration" == configurationName - - override fun processStarted(debugProcess: XDebugProcess) = - DebuggerInvocationUtil.swingInvokeLater(project) { - if (debugProcess is JavaDebugProcess) { - registerXCoroutinesPanel(debugProcess.session) - } - } - - override fun processStopped(debugProcess: XDebugProcess) { - if (processCounter.compareAndSet(1, 0)) { - connection?.disconnect() - connection = null - } else - processCounter.decrementAndGet() - } - - private fun registerXCoroutinesPanel(session: XDebugSession) { - val ui = session.ui ?: return - val xCoroutineThreadView = XCoroutineView(project, session as XDebugSessionImpl) - val framesContent: Content = createContent(ui, xCoroutineThreadView) - framesContent.isCloseable = false - ui.addContent(framesContent, 0, PlaceInGrid.right, false) - session.addSessionListener(xCoroutineThreadView.debugSessionListener(session)) - session.rebuildViews() - } - - private fun createContent(ui: RunnerLayoutUi, createContentParamProvider: CreateContentParamsProvider): Content { - val param = createContentParamProvider.createContentParams() - return ui.createContent(param.id, param.component, param.displayName, param.icon, param.parentComponent) - } -} - -val Project.coroutineConnectionListener by projectListener - -val projectListener - get() = object : ReadOnlyProperty { - lateinit var listenerProject: CoroutineProjectConnectionListener - - override fun getValue(thisRef: Project, property: KProperty<*>): CoroutineProjectConnectionListener { - if (!::listenerProject.isInitialized) - listenerProject = CoroutineProjectConnectionListener(thisRef) - return listenerProject - } - } - -internal fun coroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines") - -internal fun initializeCoroutineAgent(params: JavaParameters, it: String?) { - params.vmParametersList?.add("-javaagent:$it") - params.vmParametersList?.add("-ea") -} diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineStackFrameInterceptor.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineStackFrameInterceptor.kt index 4cf8d339729..2173223d99f 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineStackFrameInterceptor.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineStackFrameInterceptor.kt @@ -14,8 +14,12 @@ import com.intellij.xdebugger.frame.XStackFrame import com.intellij.xdebugger.impl.XDebugSessionImpl import com.sun.jdi.Location import org.jetbrains.kotlin.idea.debugger.StackFrameInterceptor +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ContinuationHolder class CoroutineStackFrameInterceptor(val project: Project) : StackFrameInterceptor { override fun createStackFrame(frame: StackFrameProxyImpl, debugProcess: DebugProcessImpl, location: Location): XStackFrame? = - null + if (AsyncStacksToggleAction.isAsyncStacksEnabled(debugProcess.xdebugProcess?.session as XDebugSessionImpl)) + ContinuationHolder.coroutineExitFrame(frame, debugProcess.debuggerContext.suspendContext as SuspendContextImpl) + else + null } \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/DebuggerConnection.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/DebuggerConnection.kt new file mode 100644 index 00000000000..56a9bf6095f --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/DebuggerConnection.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine + +import com.intellij.debugger.DebuggerInvocationUtil +import com.intellij.debugger.engine.DebugProcessImpl +import com.intellij.debugger.engine.JavaDebugProcess +import com.intellij.debugger.engine.SuspendContextImpl +import com.intellij.debugger.impl.PrioritizedTask +import com.intellij.execution.configurations.DebuggingRunnerData +import com.intellij.execution.configurations.JavaParameters +import com.intellij.execution.configurations.RunConfigurationBase +import com.intellij.execution.ui.RunnerLayoutUi +import com.intellij.execution.ui.layout.PlaceInGrid +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.registry.Registry +import com.intellij.ui.content.Content +import com.intellij.util.messages.MessageBusConnection +import com.intellij.xdebugger.XDebugProcess +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebuggerManager +import com.intellij.xdebugger.XDebuggerManagerListener +import com.intellij.xdebugger.impl.XDebugSessionImpl +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ManagerThreadExecutor +import org.jetbrains.kotlin.idea.debugger.coroutine.util.CreateContentParamsProvider +import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger +import org.jetbrains.kotlin.idea.debugger.coroutine.view.XCoroutineView + +class DebuggerConnection( + val project: Project, + val configuration: RunConfigurationBase<*>, + val params: JavaParameters?, + val runnerSettings: DebuggingRunnerData? +) : XDebuggerManagerListener { + var disposable: Disposable? = null + var connection: MessageBusConnection? = null + private val log by logger + + init { + if (params is JavaParameters) { + // gradle related logic in KotlinGradleCoroutineDebugProjectResolver + val kotlinxCoroutinesClassPathLib = + params.classPath?.pathList?.firstOrNull { it.contains("kotlinx-coroutines-debug") } + if (kotlinxCoroutinesClassPathLib is String) + initializeCoroutineAgent(params, kotlinxCoroutinesClassPathLib) + else + log.warn("'kotlinx-coroutines-debug' not found in classpath.") + } + connect() + } + + private fun initializeCoroutineAgent(params: JavaParameters, it: String?) { + params.vmParametersList?.add("-javaagent:$it") + params.vmParametersList?.add("-ea") + } + + private fun connect() { + connection = project.messageBus.connect() + connection?.subscribe(XDebuggerManager.TOPIC, this) + } + + override fun processStarted(debugProcess: XDebugProcess) = + DebuggerInvocationUtil.swingInvokeLater(project) { + if (debugProcess is JavaDebugProcess) { + disposable = registerXCoroutinesPanel(debugProcess.session) + } + } + + override fun processStopped(debugProcess: XDebugProcess) { + val rootDisposable = disposable + if (rootDisposable is Disposable && debugProcess is JavaDebugProcess && debugProcess.session.suspendContext is SuspendContextImpl) { + ManagerThreadExecutor(debugProcess).on(debugProcess.session.suspendContext).schedule { + Disposer.dispose(rootDisposable) + disposable = null + } + } + connection?.disconnect() + connection = null + } + + private fun registerXCoroutinesPanel(session: XDebugSession): Disposable? { + val ui = session.ui ?: return null + val xCoroutineThreadView = XCoroutineView(project, session as XDebugSessionImpl) + val framesContent: Content = createContent(ui, xCoroutineThreadView) + framesContent.isCloseable = false + ui.addContent(framesContent, 0, PlaceInGrid.right, false) + session.addSessionListener(xCoroutineThreadView.debugSessionListener(session)) + session.rebuildViews() + return xCoroutineThreadView + } + + private fun createContent(ui: RunnerLayoutUi, createContentParamProvider: CreateContentParamsProvider): Content { + val param = createContentParamProvider.createContentParams() + return ui.createContent(param.id, param.component, param.displayName, param.icon, param.parentComponent) + } +} + +fun standaloneCoroutineDebuggerEnabled() = Registry.`is`("kotlin.debugger.coroutines.standalone") + +fun coroutineDebuggerTraceEnabled() = Registry.`is`("kotlin.debugger.coroutines.trace") diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/DebuggerListener.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/DebuggerListener.kt new file mode 100644 index 00000000000..615117194d5 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/DebuggerListener.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine + +import com.intellij.execution.configurations.DebuggingRunnerData +import com.intellij.execution.configurations.JavaParameters +import com.intellij.execution.configurations.RunConfigurationBase +import com.intellij.execution.configurations.RunnerSettings +import com.intellij.openapi.externalSystem.service.execution.ExternalSystemRunConfiguration +import com.intellij.openapi.project.Project +import com.intellij.xdebugger.XDebuggerManagerListener +import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger + +interface DebuggerListener : XDebuggerManagerListener { + fun registerDebuggerConnection( + configuration: RunConfigurationBase<*>, + params: JavaParameters?, + runnerSettings: RunnerSettings? + ): DebuggerConnection? +} + +class CoroutineDebuggerListener(val project: Project) : DebuggerListener { + val log by logger + + override fun registerDebuggerConnection( + configuration: RunConfigurationBase<*>, + params: JavaParameters?, + runnerSettings: RunnerSettings? + ): DebuggerConnection? { + val isExternalSystemRunConfiguration = configuration is ExternalSystemRunConfiguration + val isGradleConfiguration = gradleConfiguration(configuration.type.id) + + if (runnerSettings == null || isExternalSystemRunConfiguration || isGradleConfiguration) { + log.warn("Coroutine debugger in standalone mode for ${configuration.name} ${configuration.javaClass} / ${params?.javaClass} / ${runnerSettings?.javaClass} (if enabled)") + } else if (runnerSettings is DebuggingRunnerData?) + return DebuggerConnection(project, configuration, params, runnerSettings) + return null + } + + private fun gradleConfiguration(configurationName: String) = + "GradleRunConfiguration" == configurationName || "KotlinGradleRunConfiguration" == configurationName + +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/VersionedImplementationProvider.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/VersionedImplementationProvider.kt index 992197b8b3e..ffc939238ce 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/VersionedImplementationProvider.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/VersionedImplementationProvider.kt @@ -5,8 +5,8 @@ package org.jetbrains.kotlin.idea.debugger.coroutine -import com.intellij.ui.components.JBLabel import com.intellij.ui.SimpleListCellRenderer +import com.intellij.ui.components.JBLabel import javax.swing.ListCellRenderer class VersionedImplementationProvider { diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/command/CoroutineBuilder.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/command/CoroutineBuilder.kt index ae2d46b231b..89285b4e84d 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/command/CoroutineBuilder.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/command/CoroutineBuilder.kt @@ -13,162 +13,68 @@ import com.intellij.debugger.jdi.GeneratedLocation import com.intellij.debugger.jdi.StackFrameProxyImpl import com.intellij.debugger.jdi.ThreadReferenceProxyImpl import com.intellij.util.containers.ContainerUtil -import com.intellij.xdebugger.frame.XStackFrame -import com.intellij.xdebugger.frame.XSuspendContext import com.sun.jdi.* import org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineAsyncStackTraceProvider import org.jetbrains.kotlin.idea.debugger.coroutine.data.* +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.isPreFlight import org.jetbrains.kotlin.idea.debugger.safeLineNumber import org.jetbrains.kotlin.idea.debugger.safeLocation import org.jetbrains.kotlin.idea.debugger.safeMethod -class CoroutineBuilder(val suspendContext: XSuspendContext) { +class CoroutineBuilder(val suspendContext: SuspendContextImpl) { private val coroutineStackFrameProvider = CoroutineAsyncStackTraceProvider() - val debugProcess = (suspendContext as SuspendContextImpl).debugProcess + val debugProcess = suspendContext.debugProcess private val virtualMachineProxy = debugProcess.virtualMachineProxy - private val classesByName = ClassesByNameProvider.createCache(virtualMachineProxy.allClasses()) companion object { - const val CREATION_STACK_TRACE_SEPARATOR = "\b\b\b" // the "\b\b\b" is used for creation stacktrace separator in kotlinx.coroutines + const val CREATION_STACK_TRACE_SEPARATOR = "\b\b\b" // the "\b\b\b" is used as creation stacktrace separator in kotlinx.coroutines } fun build(coroutine: CoroutineInfoData): List { val coroutineStackFrameList = mutableListOf() - val firstSuspendedStackFrameProxyImpl = firstSuspendedThreadFrame() - val creationFrameSeparatorIndex = findCreationFrameIndex(coroutine.stackTrace) - if (coroutine.state == CoroutineInfoData.State.RUNNING && coroutine.activeThread is ThreadReference) { - val threadReferenceProxyImpl = runningThreadProxy(coroutine.activeThread) - val executionStack = JavaExecutionStack(threadReferenceProxyImpl, debugProcess, suspendedSameThread(coroutine.activeThread)) + if (coroutine.isRunning() && coroutine.activeThread is ThreadReference) { + val threadReferenceProxyImpl = ThreadReferenceProxyImpl(debugProcess.virtualMachineProxy, coroutine.activeThread) - val frames = threadReferenceProxyImpl.forceFrames() + val realFrames = threadReferenceProxyImpl.forceFrames() var coroutineStackInserted = false - for (runningStackFrameProxy in frames) { - val jStackFrame = executionStack.createStackFrame(runningStackFrameProxy) - val coroutineStack = coroutineStackFrameProvider.getAsyncStackTraceSafe(runningStackFrameProxy, suspendContext) - if (coroutineStack.isNotEmpty()) { - // clue coroutine stack into the thread's real stack - - val firstMergedFrame = mergeFrameVars(coroutineStack.first(), runningStackFrameProxy, jStackFrame) - coroutineStackFrameList.add(firstMergedFrame) - - for (asyncFrame in coroutineStack.drop(1)) { - coroutineStackFrameList.add( - RestoredCoroutineStackFrameItem( - runningStackFrameProxy, - asyncFrame.location, - asyncFrame.spilledVariables - ) - ) - coroutineStackInserted = true - } - } else { - if (coroutineStackInserted && isInvokeSuspendNegativeLineMethodFrame(runningStackFrameProxy)) { - coroutineStackInserted = false - } else - coroutineStackFrameList.add(RunningCoroutineStackFrameItem(runningStackFrameProxy, jStackFrame)) + var preflightFound = false + for (runningStackFrameProxy in realFrames) { + if (runningStackFrameProxy.location().isPreFlight()) { + preflightFound = true + continue } - } - } else if ((coroutine.state == CoroutineInfoData.State - .SUSPENDED || coroutine.activeThread == null) && coroutine.lastObservedFrameFieldRef is ObjectReference - ) { - // to get frames from CoroutineInfo anyway - // the thread is paused on breakpoint - it has at least one frame - val suspendedStackTrace = coroutine.stackTrace.take(creationFrameSeparatorIndex) - for (suspendedFrame in suspendedStackTrace) { - val location = createLocation(suspendedFrame) - coroutineStackFrameList.add( - SuspendCoroutineStackFrameItem( - firstSuspendedStackFrameProxyImpl, - suspendedFrame, - coroutine.lastObservedFrameFieldRef, - location - ) - ) - } - } + if (preflightFound) { + val coroutineStack = coroutineStackFrameProvider.lookupForResumeContinuation(runningStackFrameProxy, suspendContext) + if (coroutineStack?.isNotEmpty() == true) { + // clue coroutine stack into the thread's real stack - coroutine.stackTrace.subList(creationFrameSeparatorIndex + 1, coroutine.stackTrace.size).forEach { - val location = createLocation(it) - coroutineStackFrameList.add(CreationCoroutineStackFrameItem(firstSuspendedStackFrameProxyImpl, it, location)) - } + for (asyncFrame in coroutineStack) { + coroutineStackFrameList.add( + RestoredCoroutineStackFrameItem( + runningStackFrameProxy, + asyncFrame.location, + asyncFrame.spilledVariables + ) + ) + coroutineStackInserted = true + } + } + preflightFound = false + } + if (!(coroutineStackInserted && isInvokeSuspendNegativeLineMethodFrame(runningStackFrameProxy))) + coroutineStackFrameList.add(RunningCoroutineStackFrameItem(runningStackFrameProxy)) + coroutineStackInserted = false + } + } else if ((coroutine.isSuspended() || coroutine.activeThread == null) && coroutine.lastObservedFrameFieldRef is ObjectReference) + coroutineStackFrameList.addAll(coroutine.stackTrace) + + coroutineStackFrameList.addAll(coroutine.creationStackTrace) coroutine.stackFrameList.addAll(coroutineStackFrameList) return coroutineStackFrameList } - /** - * First frames need to be merged as real frame has accurate line number but lacks local variables from coroutine-restored frame. - */ - private fun mergeFrameVars( - restoredFrame: CoroutineStackFrameItem, - runningStackFrameProxy: StackFrameProxyImpl, - jStackFrame: XStackFrame, - ): RunningCoroutineStackFrameItem { - if (restoredFrame.location is GeneratedLocation) { - val restoredMethod = restoredFrame.location.method() - val realMethod = runningStackFrameProxy.location().method() - // if refers to the same method - proceed with merge, otherwise do nothing - if (restoredMethod == realMethod) { - return RunningCoroutineStackFrameItem(runningStackFrameProxy, jStackFrame, restoredFrame.spilledVariables) - } - } - return RunningCoroutineStackFrameItem(runningStackFrameProxy, jStackFrame) - } - - private fun suspendedSameThread(activeThread: ThreadReference) = - activeThread == suspendedThreadProxy().threadReference - - private fun createLocation(stackTraceElement: StackTraceElement): Location = findLocation( - ContainerUtil.getFirstItem(classesByName[stackTraceElement.className]), - stackTraceElement.methodName, - stackTraceElement.lineNumber - ) - - private fun findLocation( - type: ReferenceType?, - methodName: String, - line: Int - ): Location { - if (type != null && line >= 0) { - try { - val location = type.locationsOfLine(DebugProcess.JAVA_STRATUM, null, line).stream() - .filter { l: Location -> l.method().name() == methodName } - .findFirst().orElse(null) - if (location != null) { - return location - } - } catch (ignored: AbsentInformationException) { - } - } - return GeneratedLocation(debugProcess, type, methodName, line) - } - - /** - * Tries to find creation frame separator if any, returns last index if none found - */ - private fun findCreationFrameIndex(frames: List): Int { - val index = frames.indexOfFirst { isCreationSeparatorFrame(it) } - return if (index < 0) - frames.lastIndex - else - index - } - - private fun isCreationSeparatorFrame(it: StackTraceElement) = - it.className.startsWith(CREATION_STACK_TRACE_SEPARATOR) - - private fun firstSuspendedThreadFrame(): StackFrameProxyImpl = - suspendedThreadProxy().forceFrames().first() - - // retrieves currently suspended but active and executing coroutine thread proxy - fun runningThreadProxy(threadReference: ThreadReference) = - ThreadReferenceProxyImpl(debugProcess.virtualMachineProxy, threadReference) - - // retrieves current suspended thread proxy - private fun suspendedThreadProxy(): ThreadReferenceProxyImpl = - (suspendContext as SuspendContextImpl).thread!! // @TODO hash replace !! - private fun isInvokeSuspendNegativeLineMethodFrame(frame: StackFrameProxyImpl) = frame.safeLocation()?.safeMethod()?.name() == "invokeSuspend" && frame.safeLocation()?.safeMethod()?.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;" && diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/command/CoroutineDumpAction.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/command/CoroutineDumpAction.kt index a0ef9ba2fc7..4fb618fbc34 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/command/CoroutineDumpAction.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/command/CoroutineDumpAction.kt @@ -25,7 +25,6 @@ import com.intellij.util.text.DateFormatUtil import com.intellij.xdebugger.impl.XDebuggerManagerImpl import org.jetbrains.kotlin.idea.debugger.coroutine.KotlinDebuggerCoroutinesBundle import org.jetbrains.kotlin.idea.debugger.coroutine.view.CoroutineDumpPanel -import org.jetbrains.kotlin.idea.debugger.coroutine.coroutineDebuggerEnabled import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.CoroutineDebugProbesProxy @@ -43,14 +42,14 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate { val states = CoroutineDebugProbesProxy(context.suspendContext ?: return) .dumpCoroutines() if (states.isOk()) { - val message = KotlinDebuggerCoroutinesBundle.message("coroutine.dump.failed") - XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(message,MessageType.ERROR).notify(project) - } else { val f = fun() { val ui = session.xDebugSession?.ui ?: return addCoroutineDump(project, states.cache, ui, session.searchScope) } ApplicationManager.getApplication().invokeLater(f, ModalityState.NON_MODAL) + } else { + val message = KotlinDebuggerCoroutinesBundle.message("coroutine.dump.failed") + XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(message,MessageType.ERROR).notify(project) } } }) @@ -93,7 +92,7 @@ class CoroutineDumpAction : AnAction(), AnAction.TransparentUpdate { return } val debuggerSession = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession - presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached && coroutineDebuggerEnabled() + presentation.isEnabled = debuggerSession != null && debuggerSession.isAttached presentation.isVisible = presentation.isEnabled } } \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/ContinuationValueDescriptorImpl.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/ContinuationValueDescriptorImpl.kt new file mode 100644 index 00000000000..4dc35ed5f5c --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/ContinuationValueDescriptorImpl.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.data + +import com.intellij.debugger.DebuggerContext +import com.intellij.debugger.engine.evaluation.EvaluateException +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl +import com.intellij.openapi.project.Project +import com.sun.jdi.Value +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ContinuationHolder + +class ContinuationValueDescriptorImpl( + project: Project, + val continuation: ContinuationHolder, + val fieldName: String, + val variableName: String +) : ValueDescriptorImpl(project) { + override fun calcValueName() = variableName + + override fun calcValue(evaluationContext: EvaluationContextImpl?): Value? { + val field = continuation.referenceType()?.fieldByName(fieldName) ?: return null + return continuation.field(field) + } + + override fun getDescriptorEvaluation(context: DebuggerContext?) = + throw EvaluateException("Spilled variable evaluation is not supported") +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineAsyncStackFrameItem.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineAsyncStackFrameItem.kt index 74013b2373a..0cb484fa114 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineAsyncStackFrameItem.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineAsyncStackFrameItem.kt @@ -1,48 +1,55 @@ /* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ package org.jetbrains.kotlin.idea.debugger.coroutine.data +import com.intellij.debugger.engine.DebugProcessImpl 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.StackFrameDescriptorImpl +import com.intellij.ui.ColoredTextContainer +import com.intellij.ui.SimpleTextAttributes +import com.intellij.xdebugger.frame.XCompositeNode import com.intellij.xdebugger.frame.XNamedValue import com.intellij.xdebugger.frame.XStackFrame import com.sun.jdi.Location import com.sun.jdi.ObjectReference import org.jetbrains.kotlin.idea.debugger.* -import org.jetbrains.kotlin.idea.debugger.coroutine.util.EmptyStackFrameDescriptor +import org.jetbrains.kotlin.idea.debugger.coroutine.coroutineDebuggerTraceEnabled import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger class CreationCoroutineStackFrameItem( - val frame: StackFrameProxyImpl, val stackTraceElement: StackTraceElement, location: Location ) : CoroutineStackFrameItem(location, emptyList()) { - fun emptyDescriptor() = + fun emptyDescriptor(frame: StackFrameProxyImpl) = EmptyStackFrameDescriptor(stackTraceElement, frame) } +/** + * Suspended frames in Suspended coroutine + */ class SuspendCoroutineStackFrameItem( - val frame: StackFrameProxyImpl, val stackTraceElement: StackTraceElement, - val lastObservedFrameFieldRef: ObjectReference, location: Location, spilledVariables: List = emptyList() ) : CoroutineStackFrameItem(location, spilledVariables) { - fun emptyDescriptor() = + fun emptyDescriptor(frame: StackFrameProxyImpl) = EmptyStackFrameDescriptor(stackTraceElement, frame) } class RunningCoroutineStackFrameItem( val frame: StackFrameProxyImpl, - val stackFrame: XStackFrame, +// val stackFrame: XStackFrame, spilledVariables: List = emptyList() ) : CoroutineStackFrameItem(frame.location(), spilledVariables) +/** + * Restored frame in Running coroutine, attaching to running thread + */ class RestoredCoroutineStackFrameItem( val frame: StackFrameProxyImpl, location: Location, @@ -52,8 +59,71 @@ class RestoredCoroutineStackFrameItem( StackFrameDescriptorImpl(frame, MethodsTracker()) } +/** + * Restored from memory dump + */ class DefaultCoroutineStackFrameItem(location: Location, spilledVariables: List) : - CoroutineStackFrameItem(location, spilledVariables) + CoroutineStackFrameItem(location, spilledVariables) { + + fun emptyDescriptor(frame: StackFrameProxyImpl) = + StackFrameDescriptorImpl(frame, MethodsTracker()) +} + +/** + * Original frame appeared before resumeWith call. + * + * Sequence is the following + * + * - KotlinStackFrame + * - invokeSuspend(KotlinStackFrame) -| + * | replaced with CoroutinePreflightStackFrame + * - resumeWith(KotlinStackFrame) ----| + * - PreCoroutineStackFrameItem part of CoroutinePreflightStackFrame + * + */ +class PreCoroutineStackFrameItem(val frame: StackFrameProxyImpl, location: Location, variables: List = emptyList()) : + CoroutineStackFrameItem(location, variables) { + constructor(frame: StackFrameProxyImpl, variables: List = emptyList()) : this(frame, frame.location(), variables) + + constructor(frame: StackFrameProxyImpl, restoredCoroutineStackFrameItem: CoroutineStackFrameItem) : this( + frame, + restoredCoroutineStackFrameItem.location, + restoredCoroutineStackFrameItem.spilledVariables + ) + + override fun createFrame(debugProcess: DebugProcessImpl): CapturedStackFrame { + return PreCoroutineStackFrame(frame, debugProcess, this) + } +} + +/** + * Can act as a joint frame, take variables form restored frame and information from the original one. + */ +class PreCoroutineStackFrame(val frame: StackFrameProxyImpl, val debugProcess: DebugProcessImpl, item: StackFrameItem) : + CoroutineStackFrame(debugProcess, item) { + override fun computeChildren(node: XCompositeNode) { + debugProcess.invokeInManagerThread { + debugProcess.getPositionManager().createStackFrame(frame, debugProcess, frame.location()) + ?.computeChildren(node) // hack but works + } + super.computeChildren(node) + } +} + +open class CoroutineStackFrame(debugProcess: DebugProcessImpl, val item: StackFrameItem) : + StackFrameItem.CapturedStackFrame(debugProcess, item) { + override fun customizePresentation(component: ColoredTextContainer) { + if (coroutineDebuggerTraceEnabled()) + component.append("${item.javaClass.simpleName} / ${this.javaClass.simpleName}", SimpleTextAttributes.GRAYED_ATTRIBUTES) + super.customizePresentation(component) + } + + override fun getCaptionAboveOf() = "CoroutineExit" + + override fun hasSeparatorAbove(): Boolean = + false +} + sealed class CoroutineStackFrameItem(val location: Location, val spilledVariables: List) : StackFrameItem(location, spilledVariables) { @@ -63,4 +133,11 @@ sealed class CoroutineStackFrameItem(val location: Location, val spilledVariable return location.safeSourceName() + ":" + location.safeMethod().toString() + ":" + location.safeLineNumber() + ":" + location.safeKotlinPreferredLineNumber() } -} \ No newline at end of file + + override fun createFrame(debugProcess: DebugProcessImpl): CapturedStackFrame { + return CoroutineStackFrame(debugProcess, this) + } +} + +class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) : + StackFrameDescriptorImpl(proxy, MethodsTracker()) diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineInfoCache.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineInfoCache.kt index b6cece541d4..bd8771ff30f 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineInfoCache.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineInfoCache.kt @@ -14,9 +14,10 @@ class CoroutineInfoCache( state = CacheState.OK } - fun fail() { + fun fail(): CoroutineInfoCache { cache.clear() state = CacheState.FAIL + return this } fun isOk(): Boolean { diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineInfoData.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineInfoData.kt index c0bd4041f65..20fe773460b 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineInfoData.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/CoroutineInfoData.kt @@ -7,17 +7,16 @@ package org.jetbrains.kotlin.idea.debugger.coroutine.data import com.sun.jdi.ObjectReference import com.sun.jdi.ThreadReference +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.CoroutineHolder /** * Represents state of a coroutine. * @see `kotlinx.coroutines.debug.CoroutineInfo` */ data class CoroutineInfoData( - val name: String, - val state: State, - - val stackTrace: List, - // links to jdi.* references + val key: CoroutineNameIdState, + val stackTrace: List, + val creationStackTrace: List, val activeThread: ThreadReference? = null, // for suspended coroutines should be null val lastObservedFrameFieldRef: ObjectReference? ) { @@ -26,22 +25,41 @@ data class CoroutineInfoData( // @TODO for refactoring/removal along with DumpPanel val stringStackTrace: String by lazy { buildString { - appendln("\"$name\", state: $state") + appendln("\"${key.name}\", state: ${key.state}") stackTrace.forEach { appendln("\t$it") } } } - fun isSuspended() = state == State.SUSPENDED + fun isSuspended() = key.state == State.SUSPENDED - fun isCreated() = state == State.CREATED + fun isCreated() = key.state == State.CREATED - fun isEmptyStackTrace() = stackTrace.isEmpty() + fun isEmptyStack() = stackTrace.isEmpty() - enum class State { - RUNNING, - SUSPENDED, - CREATED + fun isRunning() = key.state == State.RUNNING + + companion object { + fun suspendedCoroutineInfoData( + holder: CoroutineHolder, + lastObservedFrameFieldRef: ObjectReference + ): CoroutineInfoData? { + return CoroutineInfoData(holder.info, holder.stackFrameItems, emptyList(), null, lastObservedFrameFieldRef) + } } +} + +data class CoroutineNameIdState(val name: String, val id: String, val state: State) + +enum class State { + RUNNING, + SUSPENDED, + CREATED, + UNKNOWN, + SUSPENDED_COMPLETING, + SUSPENDED_CANCELLING, + CANCELLED, + COMPLETED, + NEW } \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/TreeNodeDescriptorImpl.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/TreeNodeDescriptorImpl.kt index ef30942648a..c349d8f44e9 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/TreeNodeDescriptorImpl.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/TreeNodeDescriptorImpl.kt @@ -24,18 +24,18 @@ import javax.swing.Icon class CoroutineDescriptorImpl(val infoData: CoroutineInfoData) : NodeDescriptorImpl() { lateinit var icon: Icon - override fun getName() = infoData.name + override fun getName() = infoData.key.name @Throws(EvaluateException::class) override fun calcRepresentation(context: EvaluationContextImpl?, labelListener: DescriptorLabelListener): String { val thread = infoData.activeThread - val name = thread?.name()?.substringBefore(" @${infoData.name}") ?: "" + val name = thread?.name()?.substringBefore(" @${infoData.key.name}") ?: "" val threadState = if (thread != null) DebuggerUtilsEx.getThreadStatusText(thread.status()) else "" val threadName = if (name.isNotEmpty()) " on thread \"$name\":$threadState" else "" - return "${infoData.name}: ${infoData.state} $threadName" + return "${infoData.key.name}: ${infoData.key.state} $threadName" } - override fun isExpandable() = infoData.state != CoroutineInfoData.State.CREATED + override fun isExpandable() = infoData.key.state != State.CREATED private fun calcIcon() = when { infoData.isSuspended() -> AllIcons.Debugger.ThreadSuspended diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/AsyncStackTraceContext.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/AsyncStackTraceContext.kt deleted file mode 100644 index 4ae43df50c0..00000000000 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/AsyncStackTraceContext.kt +++ /dev/null @@ -1,174 +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.coroutine.proxy - -import com.intellij.debugger.DebuggerContext -import com.intellij.debugger.engine.JavaValue -import com.intellij.debugger.engine.evaluation.EvaluateException -import com.intellij.debugger.engine.evaluation.EvaluationContextImpl -import com.intellij.debugger.jdi.GeneratedLocation -import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl -import com.intellij.xdebugger.frame.XNamedValue -import com.sun.jdi.* -import org.jetbrains.kotlin.codegen.coroutines.CONTINUATION_VARIABLE_NAME -import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext -import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger -import org.jetbrains.kotlin.idea.debugger.SUSPEND_LAMBDA_CLASSES -import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem -import org.jetbrains.kotlin.idea.debugger.coroutine.data.DefaultCoroutineStackFrameItem -import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext -import org.jetbrains.kotlin.idea.debugger.isSubtype -import org.jetbrains.kotlin.idea.debugger.safeVisibleVariableByName - -class AsyncStackTraceContext( - val context: DefaultExecutionContext, - val method: Method -) { - val log by logger - - private companion object { - const val DEBUG_METADATA_KT = "kotlin.coroutines.jvm.internal.DebugMetadataKt" - } - - fun getAsyncStackTraceIfAny(): List { - val continuation = locateContinuation() ?: return emptyList() - val frames = mutableListOf() - try { - collectFramesRecursively(continuation, frames) - } catch (e: Exception) { - log.error("Error while looking for variables.", e) - } - return frames - } - - private fun locateContinuation(): ObjectReference? { - val continuation: ObjectReference? - if (isInvokeSuspendMethod(method)) { - continuation = context.frameProxy?.thisObject() ?: return null - if (!isSuspendLambda(continuation.referenceType())) - return null - } else if (isContinuationProvider(method)) { - val continuationVariable = context.frameProxy?.safeVisibleVariableByName(CONTINUATION_VARIABLE_NAME) ?: return null - continuation = context.frameProxy?.getValue(continuationVariable) as? ObjectReference ?: return null - context.keepReference(continuation) - } else { - continuation = null - } - return continuation - } - - private fun isInvokeSuspendMethod(method: Method): Boolean = - method.name() == "invokeSuspend" && method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;" - - private fun isContinuationProvider(method: Method): Boolean = - "Lkotlin/coroutines/Continuation;)" in method.signature() - - private fun isSuspendLambda(referenceType: ReferenceType): Boolean = - SUSPEND_LAMBDA_CLASSES.any { referenceType.isSubtype(it) } - - private fun collectFramesRecursively(continuation: ObjectReference, consumer: MutableList) { - val continuationType = continuation.referenceType() as? ClassType ?: return - val baseContinuationSupertype = findBaseContinuationSuperSupertype(continuationType) ?: return - - val debugMetadataKtType = debugMetadataKtType() - if (debugMetadataKtType is ClassType) { - val location = createLocation(continuation, debugMetadataKtType) - - location?.let { - val spilledVariables = getSpilledVariables(continuation, debugMetadataKtType) ?: emptyList() - consumer.add(DefaultCoroutineStackFrameItem(location, spilledVariables)) - } - - val completionField = baseContinuationSupertype.fieldByName("completion") ?: return - val completion = continuation.getValue(completionField) as? ObjectReference ?: return - collectFramesRecursively(completion, consumer) - } - } - - fun debugMetadataKtType(): ClassType? { - val debugMetadataKtType = context.findClassSafe(DEBUG_METADATA_KT) - if (debugMetadataKtType != null) { - return debugMetadataKtType - } else { - log.warn("Continuation information found but no $DEBUG_METADATA_KT class exists. Please check kotlin-stdlib version.") - } - return null - } - - private fun createLocation(continuation: ObjectReference, debugMetadataKtType: ClassType): GeneratedLocation? { - val instance = invokeGetStackTraceElement(continuation, debugMetadataKtType) ?: return null - val className = context.invokeMethodAsString(instance, "getClassName") ?: return null - val methodName = context.invokeMethodAsString(instance, "getMethodName") ?: return null - val lineNumber = context.invokeMethodAsInt(instance, "getLineNumber")?.takeIf { - it >= 0 - } ?: return null - val locationClass = context.findClassSafe(className) ?: return null - return GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber) - } - - private fun invokeGetStackTraceElement(continuation: ObjectReference, debugMetadataKtType: ClassType): ObjectReference? { - val stackTraceElement = - context.invokeMethodAsObject(debugMetadataKtType, "getStackTraceElement", continuation) ?: return null - - stackTraceElement.referenceType().takeIf { it.name() == StackTraceElement::class.java.name } ?: return null - - context.keepReference(stackTraceElement) - return stackTraceElement - } - - fun getSpilledVariables(continuation: ObjectReference): List? { - debugMetadataKtType()?.let { - return getSpilledVariables(continuation, it) - } - return null - } - - fun getSpilledVariables(continuation: ObjectReference, debugMetadataKtType: ClassType): List? { - val rawSpilledVariables = - context.invokeMethodAsArray( - debugMetadataKtType, - "getSpilledVariableFieldMapping", - "(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;", - continuation - ) ?: return null - - context.keepReference(rawSpilledVariables) - - val length = rawSpilledVariables.length() / 2 - val spilledVariables = ArrayList(length) - - for (index in 0 until length) { - val fieldName = (rawSpilledVariables.getValue(2 * index) as? StringReference)?.value() ?: continue - val variableName = (rawSpilledVariables.getValue(2 * index + 1) as? StringReference)?.value() ?: continue - val field = continuation.referenceType().fieldByName(fieldName) ?: continue - - val valueDescriptor = object : ValueDescriptorImpl(context.project) { - override fun calcValueName() = variableName - override fun calcValue(evaluationContext: EvaluationContextImpl?) = continuation.getValue(field) - override fun getDescriptorEvaluation(context: DebuggerContext?) = - throw EvaluateException("Spilled variable evaluation is not supported") - } - - spilledVariables += JavaValue.create( - null, - valueDescriptor, - context.evaluationContext, - context.debugProcess.xdebugProcess!!.nodeManager, - false - ) - } - - return spilledVariables - } - - private tailrec fun findBaseContinuationSuperSupertype(type: ClassType): ClassType? { - if (type.name() == "kotlin.coroutines.jvm.internal.BaseContinuationImpl") { - return type - } - return findBaseContinuationSuperSupertype(type.superclass() ?: return null) - } -} - diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/ContinuationHolder.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/ContinuationHolder.kt new file mode 100644 index 00000000000..05bddb55bce --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/ContinuationHolder.kt @@ -0,0 +1,239 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.proxy + +import com.intellij.debugger.engine.JavaValue +import com.intellij.debugger.engine.SuspendContextImpl +import com.intellij.debugger.jdi.GeneratedLocation +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.xdebugger.frame.XNamedValue +import com.intellij.xdebugger.frame.XStackFrame +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.coroutine.data.ContinuationValueDescriptorImpl +import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem +import org.jetbrains.kotlin.idea.debugger.coroutine.data.DefaultCoroutineStackFrameItem +import org.jetbrains.kotlin.idea.debugger.coroutine.standaloneCoroutineDebuggerEnabled +import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger +import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext +import org.jetbrains.kotlin.idea.debugger.invokeInManagerThread + +data class ContinuationHolder(val continuation: ObjectReference, val context: DefaultExecutionContext) { + val log by logger + + fun getAsyncStackTraceIfAny(): CoroutineHolder? { + try { + return collectFrames() + } catch (e: Exception) { + log.error("Error while looking for variables.", e) + } + return null + } + + private fun collectFrames(): CoroutineHolder? { + val consumer = mutableListOf() + var completion = this + val debugMetadataKtType = debugMetadataKtType() ?: return null + while (completion.isBaseContinuationImpl()) { + val coroutineStackFrame = context.debugProcess.invokeInManagerThread { + createLocation(completion, debugMetadataKtType) + } + if (coroutineStackFrame != null) { + consumer.add(coroutineStackFrame) + } + completion = completion.findCompletion() ?: break + } + if (completion.value().type().isAbstractCoroutine()) + return CoroutineHolder.lookup(completion.value(), context, consumer) + else { + log.warn("AbstractCoroutine not found, ${completion.value().type()} is not subtype of AbstractCoroutine as expected.") + return CoroutineHolder.lookup(null, context, consumer) + } + } + + private fun createLocation(continuation: ContinuationHolder, debugMetadataKtType: ClassType): DefaultCoroutineStackFrameItem? { + val instance = invokeGetStackTraceElement(continuation, debugMetadataKtType) ?: return null + val className = context.invokeMethodAsString(instance, "getClassName") ?: return null + val methodName = context.invokeMethodAsString(instance, "getMethodName") ?: return null + val lineNumber = context.invokeMethodAsInt(instance, "getLineNumber")?.takeIf { + it >= 0 + } ?: return null // skip invokeSuspend:-1 + val locationClass = context.findClassSafe(className) ?: return null + val generatedLocation = GeneratedLocation(context.debugProcess, locationClass, methodName, lineNumber) + val spilledVariables = getSpilledVariables(continuation, debugMetadataKtType) ?: emptyList() + return DefaultCoroutineStackFrameItem(generatedLocation, spilledVariables) + } + + private fun invokeGetStackTraceElement(continuation: ContinuationHolder, debugMetadataKtType: ClassType): ObjectReference? { + val stackTraceElement = + context.invokeMethodAsObject(debugMetadataKtType, "getStackTraceElement", continuation.value()) ?: return null + + stackTraceElement.referenceType().takeIf { it.name() == StackTraceElement::class.java.name } ?: return null + context.keepReference(stackTraceElement) + return stackTraceElement + } + + fun getSpilledVariables(): List? { + debugMetadataKtType()?.let { + return getSpilledVariables(this, it) + } + return null + } + + private fun getSpilledVariables(continuation: ContinuationHolder, debugMetadataKtType: ClassType): List? { + val variables: List = context.debugProcess.invokeInManagerThread { + FieldVariable.extractFromContinuation(context, continuation.value(), debugMetadataKtType).map { + val valueDescriptor = ContinuationValueDescriptorImpl( + context.project, + continuation, + it.fieldName, + it.variableName + ) + JavaValue.create( + null, + valueDescriptor, + context.evaluationContext, + context.debugProcess.xdebugProcess!!.nodeManager, + false + ) + } + } ?: emptyList() + return variables + } + + + private fun debugMetadataKtType(): ClassType? { + val debugMetadataKtType = context.findCoroutineMetadataType() + if (debugMetadataKtType == null) + log.warn("Continuation information found but no 'kotlin.coroutines.jvm.internal.DebugMetadataKt' class exists. Please check kotlin-stdlib version.") + return debugMetadataKtType + } + + fun referenceType(): ClassType? = + continuation.referenceType() as? ClassType + + fun value() = + continuation + + fun field(field: Field): Value? = + continuation.getValue(field) + + fun findCompletion(): ContinuationHolder? { + val type = continuation.type() + if (type is ClassType && type.isBaseContinuationImpl()) { + val completionField = type.completionField() ?: return null + return ContinuationHolder(continuation.getValue(completionField) as? ObjectReference ?: return null, context) + } + return null + } + + fun isBaseContinuationImpl() = + continuation.type().isBaseContinuationImpl() + + + companion object { + val log by logger + + fun lookupForResumeMethodContinuation( + suspendContext: SuspendContextImpl, + frame: StackFrameProxyImpl + ): ContinuationHolder? { + if (frame.location().isPreExitFrame()) { + val context = suspendContext.executionContext() ?: return null + var continuation = frame.completionVariableValue() ?: return null + context.keepReference(continuation) + return ContinuationHolder(continuation, context) + } else + return null + } + + fun coroutineExitFrame( + frame: StackFrameProxyImpl, + suspendContext: SuspendContextImpl + ): XStackFrame? { + return suspendContext.invokeInManagerThread { + if (frame.location().isPreFlight()) { + if(standaloneCoroutineDebuggerEnabled()) + log.trace("Entry frame found: ${formatLocation(frame.location())}") + constructPreFlightFrame(frame, suspendContext) + } else + null + } + } + + fun constructPreFlightFrame( + invokeSuspendFrame: StackFrameProxyImpl, + suspendContext: SuspendContextImpl + ): CoroutinePreflightStackFrame? { + var frames = invokeSuspendFrame.threadProxy().frames() + val indexOfCurrentFrame = frames.indexOf(invokeSuspendFrame) + val indexofgetcoroutineSuspended = findGetCoroutineSuspended(frames) + // @TODO if found - skip this thread stack + if (indexOfCurrentFrame > 0 && frames.size > indexOfCurrentFrame && indexofgetcoroutineSuspended < 0) { + val resumeWithFrame = frames[indexOfCurrentFrame + 1] ?: return null + val ch = lookupForResumeMethodContinuation(suspendContext, resumeWithFrame) ?: return null + + val coroutineStackTrace = ch.getAsyncStackTraceIfAny() ?: return null + return CoroutinePreflightStackFrame.preflight( + invokeSuspendFrame, + resumeWithFrame, + coroutineStackTrace.stackFrameItems, + frames.drop(indexOfCurrentFrame) + ) + } + return null + } + + private fun formatLocation(location: Location): String { + return "${location.method().name()}:${location.lineNumber()}, ${location.method().declaringType()} in ${location.sourceName()}" + } + + /** + * Find continuation for the [frame] + * Gets current CoroutineInfo.lastObservedFrame and finds next frames in it until null or needed stackTraceElement is found + * @return null if matching continuation is not found or is not BaseContinuationImpl + */ + fun lookup( + context: SuspendContextImpl, + initialContinuation: ObjectReference?, +// frame: StackTraceElement, +// threadProxy: ThreadReferenceProxyImpl + ): ContinuationHolder? { + var continuation = initialContinuation ?: return null +// val classLine = ClassNameLineNumber(frame.className, frame.lineNumber) + val executionContext = context.executionContext() ?: return null + + do { +// val position = getClassAndLineNumber(executionContext, continuation) + // while continuation is BaseContinuationImpl and it's frame equals to the current + continuation = getNextFrame(executionContext, continuation) ?: return null + } while (continuation.type().isBaseContinuationImpl() /* && position != classLine */) + + return if (continuation.type().isBaseContinuationImpl()) + ContinuationHolder(continuation, executionContext) + else + return null + } + + data class ClassNameLineNumber(val className: String?, val lineNumber: Int?) +// +// private fun getClassAndLineNumber(context: ExecutionContext, continuation: ObjectReference): ClassNameLineNumber { +// val objectReference = findStackTraceElement(context, continuation) ?: return ClassNameLineNumber(null, null) +// val classStackTraceElement = context.findClass("java.lang.StackTraceElement") as ClassType +// val getClassName = classStackTraceElement.concreteMethodByName("getClassName", "()Ljava/lang/String;") +// val getLineNumber = classStackTraceElement.concreteMethodByName("getLineNumber", "()I") +// val className = (context.invokeMethod(objectReference, getClassName, emptyList()) as StringReference).value() +// val lineNumber = (context.invokeMethod(objectReference, getLineNumber, emptyList()) as IntegerValue).value() +// return ClassNameLineNumber(className, lineNumber) +// } + +// private fun findStackTraceElement(context: ExecutionContext, continuation: ObjectReference): ObjectReference? { +// val classType = continuation.type() as ClassType +// val methodGetStackTraceElement = classType.concreteMethodByName("getStackTraceElement", "()Ljava/lang/StackTraceElement;") +// return context.invokeMethod(continuation, methodGetStackTraceElement, emptyList()) as? ObjectReference +// } + } +} + diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineDebugProbesProxy.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineDebugProbesProxy.kt index ad7ec3d20e7..d3c380a5b23 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineDebugProbesProxy.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineDebugProbesProxy.kt @@ -4,48 +4,29 @@ */ package org.jetbrains.kotlin.idea.debugger.coroutine.proxy +import com.intellij.debugger.engine.DebuggerManagerThreadImpl import com.intellij.debugger.engine.SuspendContextImpl -import com.intellij.debugger.engine.evaluation.EvaluationContextImpl -import com.intellij.debugger.jdi.StackFrameProxyImpl -import com.intellij.xdebugger.frame.XSuspendContext -import com.sun.jdi.* import org.jetbrains.kotlin.idea.debugger.coroutine.command.CoroutineBuilder import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoCache import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext -import org.jetbrains.kotlin.idea.debugger.evaluate.ExecutionContext -class CoroutineDebugProbesProxy(val suspendContext: XSuspendContext) { +class CoroutineDebugProbesProxy(val suspendContext: SuspendContextImpl) { private val log by logger - private var executionContext: DefaultExecutionContext = executionContext() - // might want to use inner class but also having to monitor order of fields - private var refs: ProcessReferences = ProcessReferences(executionContext) - - companion object { - private const val DEBUG_PACKAGE = "kotlinx.coroutines.debug" - } - - @Synchronized - @Suppress("unused") - fun install() = - executionContext.invokeMethodAsVoid(refs.instance, "install") - - @Synchronized - @Suppress("unused") - fun uninstall() = - executionContext.invokeMethodAsVoid(refs.instance, "uninstall") - /** * Invokes DebugProbes from debugged process's classpath and returns states of coroutines * Should be invoked on debugger manager thread */ @Synchronized - fun dumpCoroutines() : CoroutineInfoCache { + fun dumpCoroutines(): CoroutineInfoCache { + DebuggerManagerThreadImpl.assertIsManagerThread() val coroutineInfoCache = CoroutineInfoCache() try { - val infoList = dump() + val executionContext = suspendContext.executionContext() ?: return coroutineInfoCache.fail() + val libraryAgentProxy = findProvider(executionContext) + val infoList = libraryAgentProxy.dumpCoroutinesInfo() coroutineInfoCache.ok(infoList) } catch (e: Throwable) { log.error("Exception is thrown by calling dumpCoroutines.", e) @@ -54,189 +35,8 @@ class CoroutineDebugProbesProxy(val suspendContext: XSuspendContext) { return coroutineInfoCache } - private fun dump(): List { - val coroutinesInfo = dumpCoroutinesInfo() ?: return emptyList() - - executionContext.keepReference(coroutinesInfo) - val size = sizeOf(coroutinesInfo) - - return MutableList(size) { - val elem = getElementFromList(coroutinesInfo, it) - fetchCoroutineState(elem) - } - } - - private fun dumpCoroutinesInfo() = - executionContext.invokeMethodAsObject(refs.instance, refs.dumpMethod) + private fun findProvider(executionContext: DefaultExecutionContext) = + CoroutineLibraryAgentProxy.instance(executionContext) ?: CoroutineNoLibraryProxy(executionContext) fun frameBuilder() = CoroutineBuilder(suspendContext) - - private fun getElementFromList(instance: ObjectReference, num: Int) = - executionContext.invokeMethod( - instance, refs.getRef, - listOf(executionContext.vm.virtualMachine.mirrorOf(num)) - ) as ObjectReference - - private fun fetchCoroutineState(instance: ObjectReference) : CoroutineInfoData { - val name = getName(instance) - val state = getState(instance) - val thread = getLastObservedThread(instance, refs.lastObservedThreadFieldRef) - val lastObservedFrameFieldRef = instance.getValue(refs.lastObservedFrameFieldRef) as? ObjectReference - val stackTrace = getStackTrace(instance) - return CoroutineInfoData( - name, - CoroutineInfoData.State.valueOf(state), - stackTrace, - thread, - lastObservedFrameFieldRef - ) - } - - private fun getName( - info: ObjectReference // CoroutineInfo instance - ): String { - // equals to `coroutineInfo.context.get(CoroutineName).name` - val coroutineContextInst = executionContext.invokeMethod( - info, - refs.getContextRef, - emptyList() - ) as? ObjectReference ?: throw IllegalArgumentException("Coroutine context must not be null") - val coroutineName = executionContext.invokeMethod( - coroutineContextInst, - refs.getContextElement, listOf(refs.keyFieldValueRef) - ) as? ObjectReference - // If the coroutine doesn't have a given name, CoroutineContext.get(CoroutineName) returns null - val name = if (coroutineName != null) (executionContext.invokeMethod( - coroutineName, - refs.getNameRef, - emptyList() - ) as StringReference).value() else "coroutine" - val id = (info.getValue(refs.sequenceNumberFieldRef) as LongValue).value() - return "$name#$id" - } - - private fun getState( - info: ObjectReference // CoroutineInfo instance - ): String { - // equals to `stringState = coroutineInfo.state.toString()` - val state = executionContext.invokeMethod(info, refs.getStateRef, emptyList()) as ObjectReference - return (executionContext.invokeMethod(state, refs.toString, emptyList()) as StringReference).value() - } - - private fun getLastObservedThread( - info: ObjectReference, // CoroutineInfo instance - threadRef: Field // reference to lastObservedThread - ): ThreadReference? = info.getValue(threadRef) as? ThreadReference - - /** - * Returns list of stackTraceElements for the given CoroutineInfo's [ObjectReference] - */ - private fun getStackTrace( - info: ObjectReference - ): List { - val frameList = lastObservedStackTrace(info) - val tmpList = mutableListOf() - for(it in 0 until sizeOf(frameList)) { - val frame = getElementFromList(frameList, it) - val ste = newStackTraceElement(frame) - tmpList.add(ste) - } - val mergedFrameList = enhanceStackTraceWithThreadDump(listOf(info, frameList)) - val size = sizeOf(mergedFrameList) - - val list = mutableListOf() - - for (it in 0 until size) { - val frame = getElementFromList(mergedFrameList, it) - val ste = newStackTraceElement(frame) - list.add(// 0, // add in the beginning // @TODO what's the point? - ste) - } - return list - } - - private fun newStackTraceElement(frame: ObjectReference) = - StackTraceElement( - fetchClassName(frame), - fetchMethodName(frame), - fetchFileName(frame), - fetchLine(frame) - ) - - private fun fetchLine(instance: ObjectReference) = - (instance.getValue(refs.lineNumberFieldRef) as? IntegerValue)?.value() ?: -1 - - private fun fetchFileName(instance: ObjectReference) = - (instance.getValue(refs.fileNameFieldRef) as? StringReference)?.value() ?: "" - - private fun fetchMethodName(instance: ObjectReference) = - (instance.getValue(refs.methodNameFieldRef) as? StringReference)?.value() ?: "" - - private fun fetchClassName(instance: ObjectReference) = - (instance.getValue(refs.declaringClassFieldRef) as? StringReference)?.value() ?: "" - - private fun lastObservedStackTrace(instance: ObjectReference) = - executionContext.invokeMethod(instance, refs.lastObservedStackTraceRef, emptyList()) as ObjectReference - - private fun enhanceStackTraceWithThreadDump(args: List) = - executionContext.invokeMethod( - refs.debugProbesImplInstance, - refs.enhanceStackTraceWithThreadDumpRef, args) as ObjectReference - - private fun sizeOf(args: ObjectReference): Int = - (executionContext.invokeMethod(args, refs.sizeRef, emptyList()) as IntegerValue).value() - - private fun executionContext() : DefaultExecutionContext { - return DefaultExecutionContext(suspendContext as SuspendContextImpl, suspendContext.frameProxy) - } - - /** - * @TODO refactor later - * Holds ClassTypes, Methods, ObjectReferences and Fields for a particular jvm - */ - class ProcessReferences(executionContext: DefaultExecutionContext) { - // kotlinx.coroutines.debug.DebugProbes instance and methods - val debugProbesClsRef = executionContext.findClass("$DEBUG_PACKAGE.DebugProbes") as ClassType - val debugProbesImplClsRef = executionContext.findClass("$DEBUG_PACKAGE.internal.DebugProbesImpl") as ClassType - val coroutineNameClsRef = executionContext.findClass("kotlinx.coroutines.CoroutineName") as ClassType - val classClsRef = executionContext.findClass("java.lang.Object") as ClassType - val debugProbesImplInstance = with(debugProbesImplClsRef) { getValue(fieldByName("INSTANCE")) as ObjectReference } - val enhanceStackTraceWithThreadDumpRef: Method = debugProbesImplClsRef - .methodsByName("enhanceStackTraceWithThreadDump").single() - - val dumpMethod: Method = debugProbesClsRef.concreteMethodByName("dumpCoroutinesInfo", "()Ljava/util/List;") - val instance = with(debugProbesClsRef) { getValue(fieldByName("INSTANCE")) as ObjectReference } - - // CoroutineInfo - val coroutineInfoClsRef = executionContext.findClass("$DEBUG_PACKAGE.CoroutineInfo") as ClassType - val coroutineContextClsRef = executionContext.findClass("kotlin.coroutines.CoroutineContext") as InterfaceType - - val getStateRef: Method = coroutineInfoClsRef.concreteMethodByName("getState", "()Lkotlinx/coroutines/debug/State;") - val getContextRef: Method = coroutineInfoClsRef.concreteMethodByName("getContext", "()Lkotlin/coroutines/CoroutineContext;") - val sequenceNumberFieldRef: Field = coroutineInfoClsRef.fieldByName("sequenceNumber") - val lastObservedStackTraceRef: Method = coroutineInfoClsRef.methodsByName("lastObservedStackTrace").single() - val getContextElement: Method = coroutineContextClsRef.methodsByName("get").single() - val getNameRef: Method = coroutineNameClsRef.methodsByName("getName").single() - val keyFieldRef = coroutineNameClsRef.fieldByName("Key") - val toString: Method = classClsRef.concreteMethodByName("toString", "()Ljava/lang/String;") - - val lastObservedThreadFieldRef: Field = coroutineInfoClsRef.fieldByName("lastObservedThread") - val lastObservedFrameFieldRef: Field = coroutineInfoClsRef.fieldByName("lastObservedFrame") // continuation - - // Methods for list - val listClsRef = executionContext.findClass("java.util.List") as InterfaceType - val sizeRef: Method = listClsRef.methodsByName("size").single() - val getRef: Method = listClsRef.methodsByName("get").single() - val stackTraceElementClsRef = executionContext.findClass("java.lang.StackTraceElement") as ClassType - - // for StackTraceElement - val methodNameFieldRef: Field = stackTraceElementClsRef.fieldByName("methodName") - val declaringClassFieldRef: Field = stackTraceElementClsRef.fieldByName("declaringClass") - val fileNameFieldRef: Field = stackTraceElementClsRef.fieldByName("fileName") - val lineNumberFieldRef: Field = stackTraceElementClsRef.fieldByName("lineNumber") - - // value - val keyFieldValueRef = coroutineNameClsRef.getValue(keyFieldRef) as ObjectReference - - } } \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineHolder.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineHolder.kt new file mode 100644 index 00000000000..3720d7e873c --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineHolder.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.proxy + +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineNameIdState +import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem +import org.jetbrains.kotlin.idea.debugger.coroutine.data.State +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.JavaLangMirror +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.MirrorOfCoroutineContext +import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext + +data class CoroutineHolder( + val value: ObjectReference?, + val info: CoroutineNameIdState, + val stackFrameItems: List +) { + companion object { + fun lookup( + value: ObjectReference?, + context: DefaultExecutionContext, + stackFrameItems: List + ): CoroutineHolder? { + val state = state(value, context) ?: return null + val realState = + if (stackFrameItems.isEmpty()) state.copy(state = State.CREATED) else state.copy(state = State.SUSPENDED) + return CoroutineHolder(value, realState, stackFrameItems) + } + + fun state(value: ObjectReference?, context: DefaultExecutionContext): CoroutineNameIdState? { + value ?: return null + val reference = JavaLangMirror(context) + val standAloneCoroutineMirror = reference.standaloneCoroutine.mirror(value, context) + if (standAloneCoroutineMirror?.context is MirrorOfCoroutineContext) { + val id = standAloneCoroutineMirror.context.id + val toString = reference.string(value, context) + val r = """\w+\{(\w+)\}\@([\w\d]+)""".toRegex() + val matcher = r.toPattern().matcher(toString) + if (matcher.matches()) { + val state = stateOf(matcher.group(1)) + val hexAddress = matcher.group(2) + return CoroutineNameIdState(standAloneCoroutineMirror.context.name, id?.toString() ?: hexAddress, state) + } + } + return null + } + + private fun stateOf(state: String?): State = + when (state) { + "Active" -> State.RUNNING + "Cancelling" -> State.SUSPENDED_CANCELLING + "Completing" -> State.SUSPENDED_COMPLETING + "Cancelled" -> State.CANCELLED + "Completed" -> State.COMPLETED + "New" -> State.NEW + else -> State.UNKNOWN + } + } +} + + diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineInfoProvider.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineInfoProvider.kt new file mode 100644 index 00000000000..f4a4ff4341e --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineInfoProvider.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.proxy + +import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData + +interface CoroutineInfoProvider { + fun dumpCoroutinesInfo(): List +} diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineLibraryAgentProxy.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineLibraryAgentProxy.kt new file mode 100644 index 00000000000..9530345afb2 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineLibraryAgentProxy.kt @@ -0,0 +1,220 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.proxy + +import com.intellij.debugger.engine.DebugProcess +import com.intellij.debugger.engine.evaluation.EvaluateException +import com.intellij.debugger.jdi.ClassesByNameProvider +import com.intellij.debugger.jdi.GeneratedLocation +import com.intellij.util.containers.ContainerUtil +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.coroutine.data.* +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.CoroutineContext +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.JavaLangMirror +import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext + +class CoroutineLibraryAgentProxy(private val debugProbesClsRef: ClassType, private val executionContext: DefaultExecutionContext) : + CoroutineInfoProvider { + private val coroutineContextReference: JavaLangMirror = JavaLangMirror(executionContext) + + private val debugProbesImplClsRef = executionContext.findClass("$DEBUG_PACKAGE.internal.DebugProbesImpl") as ClassType + private val debugProbesImplInstance = with(debugProbesImplClsRef) { getValue(fieldByName("INSTANCE")) as ObjectReference } + private val enhanceStackTraceWithThreadDumpRef: Method = debugProbesImplClsRef + .methodsByName("enhanceStackTraceWithThreadDump").single() + + private val dumpMethod: Method = debugProbesClsRef.concreteMethodByName("dumpCoroutinesInfo", "()Ljava/util/List;") + val instance = with(debugProbesClsRef) { getValue(fieldByName("INSTANCE")) as ObjectReference } + + // CoroutineInfo + private val coroutineInfoClsRef = executionContext.findClass("$DEBUG_PACKAGE.CoroutineInfo") as ClassType + + private val getStateRef: Method = coroutineInfoClsRef.concreteMethodByName("getState", "()Lkotlinx/coroutines/debug/State;") + private val getContextRef: Method = coroutineInfoClsRef.concreteMethodByName("getContext", "()Lkotlin/coroutines/CoroutineContext;") + private val lastObservedStackTraceRef: Method = coroutineInfoClsRef.methodsByName("lastObservedStackTrace").single() + + private val sequenceNumberFieldRef: Field = coroutineInfoClsRef.fieldByName("sequenceNumber") + private val lastObservedThreadFieldRef: Field = coroutineInfoClsRef.fieldByName("lastObservedThread") + private val lastObservedFrameFieldRef: Field = coroutineInfoClsRef.fieldByName("lastObservedFrame") // continuation + + // value + private val vm = executionContext.vm + private val classesByName = ClassesByNameProvider.createCache(vm.allClasses()) + + + private val coroutineContext: CoroutineContext = CoroutineContext(executionContext) + + @Synchronized + @Suppress("unused") + fun install() = + executionContext.invokeMethodAsVoid(instance, "install") + + @Synchronized + @Suppress("unused") + fun uninstall() = + executionContext.invokeMethodAsVoid(instance, "uninstall") + + override fun dumpCoroutinesInfo(): List { + val coroutinesInfo = executionContext.invokeMethodAsObject(instance, dumpMethod) ?: return emptyList() + executionContext.keepReference(coroutinesInfo) + val size = coroutineContextReference.sizeOf(coroutinesInfo, executionContext) + + return MutableList(size) { + val elem = coroutineContextReference.elementFromList(coroutinesInfo, it, executionContext) + fetchCoroutineState(elem) + } + } + + private fun fetchCoroutineState(instance: ObjectReference): CoroutineInfoData { + val name = getName(instance) + val state = getState(instance) + val thread = getLastObservedThread(instance, lastObservedThreadFieldRef) + val lastObservedFrameFieldRef = instance.getValue(lastObservedFrameFieldRef) as? ObjectReference + val stackTrace = getStackTrace(instance) + val creationFrameSeparatorIndex = findCreationFrameIndex(stackTrace) + val coroutineStackTrace = stackTrace.take(creationFrameSeparatorIndex) + + val coroutineStackTraceFrameItems = coroutineStackTrace.map { + SuspendCoroutineStackFrameItem(it, createLocation(it)) + } + val creationStackTrace = stackTrace.subList(creationFrameSeparatorIndex + 1, stackTrace.size) + val creationStackTraceFrameItems = creationStackTrace.map { + CreationCoroutineStackFrameItem(it, createLocation(it)) + } + val key = CoroutineNameIdState(name,"", State.valueOf(state)) + + return CoroutineInfoData( + key, + coroutineStackTraceFrameItems, + creationStackTraceFrameItems, + thread, + lastObservedFrameFieldRef + ) + } + + + private fun createLocation(stackTraceElement: StackTraceElement): Location = findLocation( + ContainerUtil.getFirstItem(classesByName[stackTraceElement.className]), + stackTraceElement.methodName, + stackTraceElement.lineNumber + ) + + private fun findLocation( + type: ReferenceType?, + methodName: String, + line: Int + ): Location { + if (type != null && line >= 0) { + try { + val location = type.locationsOfLine(DebugProcess.JAVA_STRATUM, null, line).stream() + .filter { l: Location -> l.method().name() == methodName } + .findFirst().orElse(null) + if (location != null) { + return location + } + } catch (ignored: AbsentInformationException) { + } + } + return GeneratedLocation(executionContext.debugProcess, type, methodName, line) + } + + /** + * Tries to find creation frame separator if any, returns last index if none found + */ + private fun findCreationFrameIndex(frames: List): Int { + val index = frames.indexOfFirst { it.isCreationSeparatorFrame() } + return if (index < 0) + frames.lastIndex + else + index + } + + private fun getName( + info: ObjectReference // CoroutineInfo instance + ): String { + // equals to `coroutineInfo.context.get(CoroutineName).name` + val coroutineContextInst = executionContext.invokeMethod( + info, + getContextRef, + emptyList() + ) as? ObjectReference ?: throw IllegalArgumentException("Coroutine context must not be null") + val context = coroutineContext.mirror(coroutineContextInst, executionContext) + val name = context?.name ?: "coroutine" + val id = (info.getValue(sequenceNumberFieldRef) as LongValue).value() + return "$name#$id" + } + + private fun getState( + info: ObjectReference // CoroutineInfo instance + ): String { + // equals to `stringState = coroutineInfo.state.toString()` + val state = executionContext.invokeMethod(info, getStateRef, emptyList()) as ObjectReference + return coroutineContextReference.string(state, executionContext) + } + + private fun getLastObservedThread( + info: ObjectReference, // CoroutineInfo instance + threadRef: Field // reference to lastObservedThread + ): ThreadReference? = info.getValue(threadRef) as? ThreadReference + + /** + * Returns list of stackTraceElements for the given CoroutineInfo's [ObjectReference] + */ + private fun getStackTrace( + info: ObjectReference + ): List { + val frameList = lastObservedStackTrace(info) +// val tmpList = mutableListOf() +// val sizeOfFrameList = coroutineContextReference.sizeOf(frameList, executionContext) +// for (it in 0 until sizeOfFrameList) { +// val frame = coroutineContextReference.elementFromList(frameList, it, executionContext) +// val ste = coroutineContextReference.stackTraceElement(frame) +// tmpList.add(ste) +// } + val mergedFrameList = enhanceStackTraceWithThreadDump(listOf(info, frameList)) + val sizeOfMergedFrameList = coroutineContextReference.sizeOf(mergedFrameList, executionContext) + + val list = mutableListOf() + + for (it in 0 until sizeOfMergedFrameList) { + val frame = coroutineContextReference.elementFromList(mergedFrameList, it, executionContext) + val ste = coroutineContextReference.stackTraceElement(frame) + list.add(// 0, // add in the beginning // @TODO what's the point? + ste + ) + } + return list + } + + private fun lastObservedStackTrace(instance: ObjectReference) = + executionContext.invokeMethod(instance, lastObservedStackTraceRef, emptyList()) as ObjectReference + + private fun enhanceStackTraceWithThreadDump(args: List) = + executionContext.invokeMethod( + debugProbesImplInstance, + enhanceStackTraceWithThreadDumpRef, args + ) as ObjectReference + + companion object { + private const val DEBUG_PACKAGE = "kotlinx.coroutines.debug" + + fun instance(executionContext: DefaultExecutionContext): CoroutineLibraryAgentProxy? { + try { + val debugProbesClsRef = executionContext.findClass("$DEBUG_PACKAGE.DebugProbes") ?: return null + if (debugProbesClsRef is ClassType) { + val instanceField = debugProbesClsRef.fieldByName("INSTANCE") + val debugProbes = debugProbesClsRef.getValue(instanceField) as ObjectReference + val f = debugProbes.referenceType().fieldByName("isInstalled") + val debugProbesActivated = if (f != null) (debugProbes.getValue(f) as BooleanValue).value() else true + if (debugProbesActivated) + return CoroutineLibraryAgentProxy(debugProbesClsRef, executionContext) + } + } catch (e: EvaluateException) { + } + return null + } + } + +} diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineNoLibraryProxy.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineNoLibraryProxy.kt new file mode 100644 index 00000000000..c4ec68e333b --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineNoLibraryProxy.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.proxy + +import com.intellij.openapi.util.registry.Registry +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.CancellableContinuationImpl +import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger +import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext + +class CoroutineNoLibraryProxy(val executionContext: DefaultExecutionContext) : CoroutineInfoProvider { + val log by logger + val debugMetadataKtType = executionContext.findCoroutineMetadataType() + + override fun dumpCoroutinesInfo(): List { + val vm = executionContext.vm + val resultList = mutableListOf() + if (vm.virtualMachine.canGetInstanceInfo()) { + when (coroutineSwitch()) { + "DISPATCHED_CONTINUATION" -> dispatchedContinuation(resultList) + "CANCELLABLE_CONTINUATION" -> cancellableContinuation(resultList) + else -> dispatchedContinuation(resultList) + } + + } else + log.warn("Remote JVM doesn't support canGetInstanceInfo capability (perhaps JDK-8197943).") + return resultList + } + + private fun cancellableContinuation(resultList: MutableList): Boolean { + val dcClassTypeList = executionContext.findCancellableContinuationImplReferenceType() + if (dcClassTypeList?.size == 1) { + val dcClassType = dcClassTypeList.first() + val cci = CancellableContinuationImpl(executionContext) + + val continuationList = dcClassType.instances(maxCoroutines()) + for (cancellableContinuation in continuationList) { + val coroutineInfo = extractCancellableContinuation(cancellableContinuation, cci) ?: continue + resultList.add(coroutineInfo) + } + } + return false + } + + fun extractCancellableContinuation( + dispatchedContinuation: ObjectReference, + ccMirrorProvider: CancellableContinuationImpl + ): CoroutineInfoData? { + val mirror = ccMirrorProvider.mirror(dispatchedContinuation, executionContext) ?: return null + val continuation = mirror.delegate?.continuation ?: return null + val ch = ContinuationHolder(continuation, executionContext) + val coroutineWithRestoredStack = ch.getAsyncStackTraceIfAny() ?: return null + return CoroutineInfoData.suspendedCoroutineInfoData(coroutineWithRestoredStack, continuation) + } + + private fun dispatchedContinuation(resultList: MutableList): Boolean { + val dcClassTypeList = executionContext.findDispatchedContinuationReferenceType() + if (dcClassTypeList?.size == 1) { + val dcClassType = dcClassTypeList.first() + val continuationField = dcClassType.fieldByName("continuation") ?: return true + val continuationList = dcClassType.instances(maxCoroutines()) + for (dispatchedContinuation in continuationList) { + val coroutineInfo = extractDispatchedContinuation(dispatchedContinuation, continuationField) ?: continue + resultList.add(coroutineInfo) + } + } + return false + } + + fun extractDispatchedContinuation(dispatchedContinuation: ObjectReference, continuation: Field): CoroutineInfoData? { + debugMetadataKtType ?: return null + val initialContinuation = dispatchedContinuation.getValue(continuation) as ObjectReference + val ch = ContinuationHolder(initialContinuation, executionContext) + val coroutineWithRestoredStack = ch.getAsyncStackTraceIfAny() ?: return null + return CoroutineInfoData.suspendedCoroutineInfoData(coroutineWithRestoredStack, initialContinuation) + } + +} + +fun maxCoroutines() = Registry.intValue("kotlin.debugger.coroutines.max", 1000).toLong() + +fun coroutineSwitch() = Registry.stringValue("kotlin.debugger.coroutines.switch") diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutinePreflightStackFrame.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutinePreflightStackFrame.kt new file mode 100644 index 00000000000..237c922f907 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutinePreflightStackFrame.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.proxy + +import com.intellij.debugger.engine.DebugProcessImpl +import com.intellij.debugger.engine.DebuggerManagerThreadImpl +import com.intellij.debugger.engine.JVMStackFrameInfoProvider +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.debugger.ui.impl.watch.MethodsTracker +import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl +import com.intellij.xdebugger.frame.XCompositeNode +import com.intellij.xdebugger.frame.XValueChildrenList +import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem +import org.jetbrains.kotlin.idea.debugger.invokeInManagerThread +import org.jetbrains.kotlin.idea.debugger.stackFrame.KotlinStackFrame + +/** + * Coroutine exit frame represented by a stack frames + * invokeSuspend():-1 + * resumeWith() + * + */ + +class CoroutinePreflightStackFrame( + val invokeSuspendFrame: StackFrameProxyImpl, + val resumeWithFrame: StackFrameProxyImpl, + val restoredStackFrame: List, + val stackFrameDescriptorImpl: StackFrameDescriptorImpl, + val threadPreCoroutineFrames: List +) : KotlinStackFrame(stackFrameDescriptorImpl), JVMStackFrameInfoProvider { + + override fun computeChildren(node: XCompositeNode) { + val childrenList = XValueChildrenList() + val firstRestoredCoroutineStackFrameItem = restoredStackFrame.firstOrNull() ?: return + firstRestoredCoroutineStackFrameItem.spilledVariables.forEach { + childrenList.add(it) + } + node.addChildren(childrenList, false) + super.computeChildren(node) + } + + override fun isInLibraryContent() = + false + + override fun isSynthetic() = + false + + companion object { + fun preflight( + invokeSuspendFrame: StackFrameProxyImpl, + resumeWithFrame: StackFrameProxyImpl, + restoredStackFrame: List, + originalFrames: List + ): CoroutinePreflightStackFrame? { + val topRestoredFrame = restoredStackFrame.firstOrNull() ?: return null + val descriptor = StackFrameDescriptorImpl( + LocationStackFrameProxyImpl(topRestoredFrame.location, invokeSuspendFrame), MethodsTracker() + ) + return CoroutinePreflightStackFrame(invokeSuspendFrame, resumeWithFrame, restoredStackFrame, descriptor, originalFrames) + } + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineUtils.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineUtils.kt new file mode 100644 index 00000000000..4ffc9214845 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineUtils.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.proxy + +import com.intellij.debugger.engine.SuspendContextImpl +import com.intellij.debugger.engine.evaluation.EvaluationContextImpl +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.debugger.jdi.ThreadReferenceProxyImpl +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.XDebuggerUtil +import com.intellij.xdebugger.XSourcePosition +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.* +import org.jetbrains.kotlin.idea.debugger.coroutine.command.CoroutineBuilder +import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext +import org.jetbrains.kotlin.idea.util.application.isUnitTestMode + +fun Method.isInvokeSuspend(): Boolean = + name() == "invokeSuspend" && signature() == "(Ljava/lang/Object;)Ljava/lang/Object;" + +fun Method.isContinuation() = + isInvokeSuspend() && declaringType().isContinuation() /* Perhaps need to check for "Lkotlin/coroutines/Continuation;)" in signature() ? */ + +fun Method.isSuspendLambda() = + isInvokeSuspend() && declaringType().isSuspendLambda() + +fun Method.isResumeWith() = + name() == "resumeWith" && signature() == "(Ljava/lang/Object;)V" && (declaringType().isSuspendLambda() || declaringType().isContinuation()) + +fun Location.isPreFlight(): Boolean { + val method = safeMethod() ?: return false + return method.isSuspendLambda() || method.isContinuation() +} + +fun ReferenceType.isContinuation() = + isBaseContinuationImpl() || isSubtype("kotlin.coroutines.Continuation") + +fun Type.isBaseContinuationImpl() = + isSubtype("kotlin.coroutines.jvm.internal.BaseContinuationImpl") + +fun Type.isAbstractCoroutine() = + isSubtype("kotlinx.coroutines.AbstractCoroutine") + +fun Type.isSubTypeOrSame(className: String) = + name() == className || isSubtype(className) + +fun ReferenceType.isSuspendLambda() = + SUSPEND_LAMBDA_CLASSES.any { isSubtype(it) } + +fun Location.isPreExitFrame() = + safeMethod()?.isResumeWith() ?: false + +fun StackFrameProxyImpl.variableValue(variableName: String): ObjectReference? { + val continuationVariable = safeVisibleVariableByName(variableName) ?: return null + return getValue(continuationVariable) as? ObjectReference ?: return null +} + +fun StackFrameProxyImpl.completionVariableValue(): ObjectReference? = + variableValue("completion") + +private fun Method.isGetCOROUTINE_SUSPENDED() = + signature() == "()Ljava/lang/Object;" && name() == "getCOROUTINE_SUSPENDED" && declaringType().name() == "kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsKt" + +fun DefaultExecutionContext.findCoroutineMetadataType() = + debugProcess.invokeInManagerThread { findClassSafe("kotlin.coroutines.jvm.internal.DebugMetadataKt") } + +fun DefaultExecutionContext.findCoroutineAnnotationMetadataType() = + debugProcess.invokeInManagerThread { findClassSafe("kotlin.coroutines.jvm.internal.DebugMetadata") as InterfaceType? } + +fun DefaultExecutionContext.findDispatchedContinuationReferenceType(): List? = + vm.classesByName("kotlinx.coroutines.DispatchedContinuation") + +fun DefaultExecutionContext.findCancellableContinuationImplReferenceType(): List? = + vm.classesByName("kotlinx.coroutines.CancellableContinuationImpl") + +fun findGetCoroutineSuspended(frames: List) = + frames.indexOfFirst { it.safeLocation()?.safeMethod()?.isGetCOROUTINE_SUSPENDED() == true } + +fun StackTraceElement.isCreationSeparatorFrame() = + className.startsWith(CoroutineBuilder.CREATION_STACK_TRACE_SEPARATOR) + +fun StackTraceElement.findPosition(project: Project): XSourcePosition? = + getPosition(project, className, lineNumber) + +fun Location.findPosition(project: Project) = + getPosition(project, declaringType().name(), lineNumber()) + +fun ClassType.completionField() = + fieldByName("completion") as Field? + +private fun getPosition(project: Project, className: String, lineNumber: Int): XSourcePosition? { + val psiFacade = JavaPsiFacade.getInstance(project) + val psiClass = psiFacade.findClass( + className.substringBefore("$"), // find outer class, for which psi exists TODO + GlobalSearchScope.everythingScope(project) + ) + val classFile = psiClass?.containingFile?.virtualFile + // to convert to 0-based line number or '-1' to do not move + val localLineNumber = if (lineNumber > 0) lineNumber - 1 else return null + return XDebuggerUtil.getInstance().createPosition(classFile, localLineNumber) +} +/** + * Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl) + * @return null if given ObjectReference is not a BaseContinuationImpl instance or completion is null + */ +fun getNextFrame(context: DefaultExecutionContext, continuation: ObjectReference): ObjectReference? { + if (!continuation.type().isBaseContinuationImpl()) + return null + val type = continuation.type() as ClassType + val next = type.concreteMethodByName("getCompletion", "()Lkotlin/coroutines/Continuation;") + return context.invokeMethod(continuation, next, emptyList()) as? ObjectReference +} + +fun SuspendContextImpl.executionContext() = + invokeInManagerThread { DefaultExecutionContext(EvaluationContextImpl(this, this.frameProxy)) } + +fun SuspendContextImpl.invokeInManagerThread(f: () -> T?) : T? = + debugProcess.invokeInManagerThread { f() } + +fun ThreadReferenceProxyImpl.supportsEvaluation(): Boolean = + threadReference?.isSuspended ?: false + +fun SuspendContextImpl.supportsEvaluation() = + this.debugProcess.canRunEvaluation || isUnitTestMode() + +fun XDebugSession.suspendContextImpl() = + suspendContext as SuspendContextImpl diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/FieldVariable.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/FieldVariable.kt new file mode 100644 index 00000000000..ea58fca8454 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/FieldVariable.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.proxy + +import com.sun.jdi.ArrayReference +import com.sun.jdi.ClassType +import com.sun.jdi.ObjectReference +import com.sun.jdi.StringReference +import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext + +data class FieldVariable(val fieldName: String, val variableName: String) { + companion object { + fun extractFromContinuation(context: DefaultExecutionContext, continuation: ObjectReference, debugMetadataKtType: ClassType?) : List { + val metadataType = debugMetadataKtType ?: context.findCoroutineMetadataType() ?: return emptyList() + val rawSpilledVariables = + context.invokeMethodAsArray( + metadataType, + "getSpilledVariableFieldMapping", + "(Lkotlin/coroutines/jvm/internal/BaseContinuationImpl;)[Ljava/lang/String;", + continuation + ) ?: return emptyList() + + context.keepReference(rawSpilledVariables) + + val length = rawSpilledVariables.length() / 2 + val fieldVariables = ArrayList() + for (index in 0 until length) { + fieldVariables.add(getFieldVariableName(rawSpilledVariables, index) ?: continue) + } + return fieldVariables + } + + + private fun getFieldVariableName(rawSpilledVariables: ArrayReference, index: Int): FieldVariable? { + val fieldName = (rawSpilledVariables.getValue(2 * index) as? StringReference)?.value() ?: return null + val variableName = (rawSpilledVariables.getValue(2 * index + 1) as? StringReference)?.value() ?: return null + return FieldVariable(fieldName, variableName) + } + } +} + diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/LocationStackFrameProxyImpl.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/LocationStackFrameProxyImpl.kt new file mode 100644 index 00000000000..ca109e656f6 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/LocationStackFrameProxyImpl.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.proxy + +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.sun.jdi.Location + +class LocationStackFrameProxyImpl(val location: Location, frame: StackFrameProxyImpl) : + StackFrameProxyImpl(frame.threadProxy(), frame.stackFrame, frame.indexFromBottom) { + override fun location(): Location { + return location + } +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/LookupContinuation.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/LookupContinuation.kt deleted file mode 100644 index 6f38b73e040..00000000000 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/LookupContinuation.kt +++ /dev/null @@ -1,94 +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.coroutine.proxy - -import com.sun.jdi.* -import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext -import org.jetbrains.kotlin.idea.debugger.isSubtype - -class LookupContinuation(val context: DefaultExecutionContext, val frame: StackTraceElement) { - - private fun suspendOrInvokeSuspend(method: Method): Boolean = - "Lkotlin/coroutines/Continuation;)" in method.signature() || - (method.name() == "invokeSuspend" && method.signature() == "(Ljava/lang/Object;)Ljava/lang/Object;") // suspend fun or invokeSuspend - - private fun findMethod(): Method { - val clazz = context.findClass(frame.className) as ClassType - val method = clazz.methodsByName(frame.methodName).last { - val loc = it.location().lineNumber() - loc < 0 && frame.lineNumber < 0 || loc > 0 && loc <= frame.lineNumber - } // pick correct method if an overloaded one is given - return method - } - - fun isApplicable(): Boolean { - val method = findMethod() - return suspendOrInvokeSuspend(method) - } - - /** - * Find continuation for the [frame] - * Gets current CoroutineInfo.lastObservedFrame and finds next frames in it until null or needed stackTraceElement is found - * @return null if matching continuation is not found or is not BaseContinuationImpl - */ - fun findContinuation(initialContinuation: ObjectReference?): ObjectReference? { - if (!isApplicable()) - return null - - var continuation = initialContinuation ?: return null - val baseType = "kotlin.coroutines.jvm.internal.BaseContinuationImpl" - val getTrace = (continuation.type() as ClassType).concreteMethodByName( - "getStackTraceElement", - "()Ljava/lang/StackTraceElement;" - ) - val stackTraceType = context.findClass("java.lang.StackTraceElement") as ClassType - val getClassName = stackTraceType.concreteMethodByName("getClassName", "()Ljava/lang/String;") - val getLineNumber = stackTraceType.concreteMethodByName("getLineNumber", "()I") - val className = { - val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference - if (trace != null) - (context.invokeMethod(trace, getClassName, emptyList()) as StringReference).value() - else null - } - val lineNumber = { - val trace = context.invokeMethod(continuation, getTrace, emptyList()) as? ObjectReference - if (trace != null) - (context.invokeMethod(trace, getLineNumber, emptyList()) as IntegerValue).value() - else null - } - - while (continuation.type().isSubtype(baseType) - && (frame.className != className() || frame.lineNumber != lineNumber()) - ) { - // while continuation is BaseContinuationImpl and it's frame equals to the current - continuation = getNextFrame(continuation, context) ?: return null - } - return if (continuation.type().isSubtype(baseType)) continuation else null - } - - - /** - * Finds previous Continuation for this Continuation (completion field in BaseContinuationImpl) - * @return null if given ObjectReference is not a BaseContinuationImpl instance or completion is null - */ - private fun getNextFrame(continuation: ObjectReference, context: DefaultExecutionContext): 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 - } - - fun findGetStackTraceElementMethodRef(continuation: ObjectReference): Method = - (continuation.type() as ClassType).concreteMethodByName("getStackTraceElement", "()Ljava/lang/StackTraceElement;") - - fun createAsyncStackTraceContext(stackTraceElementMethodRef: Method) = - AsyncStackTraceContext(context, stackTraceElementMethodRef) - - fun createAsyncStackTraceContext(continuation: ObjectReference): AsyncStackTraceContext? { - val getStackTraceElementMethodRef = findGetStackTraceElementMethodRef(continuation) - return createAsyncStackTraceContext(getStackTraceElementMethodRef) - } -} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/ManagerThreadExecutor.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/ManagerThreadExecutor.kt index 1fbd75b1a1c..2cdb62e98b0 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/ManagerThreadExecutor.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/ManagerThreadExecutor.kt @@ -6,28 +6,34 @@ package org.jetbrains.kotlin.idea.debugger.coroutine.proxy import com.intellij.debugger.engine.DebugProcessImpl +import com.intellij.debugger.engine.JavaDebugProcess import com.intellij.debugger.engine.SuspendContextImpl -import com.intellij.debugger.engine.events.DebuggerCommandImpl import com.intellij.debugger.engine.events.SuspendContextCommandImpl import com.intellij.debugger.impl.PrioritizedTask import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ModalityState import com.intellij.openapi.util.Computable +import com.intellij.xdebugger.XDebugProcess +import com.intellij.xdebugger.XDebugSession import com.intellij.xdebugger.frame.XSuspendContext import java.awt.Component class ManagerThreadExecutor(val debugProcess: DebugProcessImpl) { - fun on(suspendContext: SuspendContextImpl, priority: PrioritizedTask.Priority = PrioritizedTask.Priority.NORMAL) = - ManagerThreadExecutorInstance(suspendContext, priority) + + constructor(session: XDebugSession) : this(session.debugProcess) + + constructor(debugProcess: XDebugProcess) : this((debugProcess as JavaDebugProcess).debuggerSession.process) fun on(suspendContext: XSuspendContext, priority: PrioritizedTask.Priority = PrioritizedTask.Priority.NORMAL) = - ManagerThreadExecutorInstance(suspendContext as SuspendContextImpl, priority) + ManagerThreadExecutorInstance(suspendContext, priority) inner class ManagerThreadExecutorInstance( val suspendContext: SuspendContextImpl, val priority: PrioritizedTask.Priority = PrioritizedTask.Priority.NORMAL ) { + constructor(sc: XSuspendContext, priority: PrioritizedTask.Priority) : this(sc as SuspendContextImpl, priority) + fun schedule(f: (SuspendContextImpl) -> Unit) { val suspendContextCommand = object : SuspendContextCommandImpl(suspendContext) { override fun getPriority() = this@ManagerThreadExecutorInstance.priority diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/mirror/coroutinesContextMirror.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/mirror/coroutinesContextMirror.kt new file mode 100644 index 00000000000..8fac8c2c87e --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/mirror/coroutinesContextMirror.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror + +import com.sun.jdi.Method +import com.sun.jdi.ObjectReference +import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext + +class CoroutineContext(context: DefaultExecutionContext) : + BaseMirror("kotlin.coroutines.CoroutineContext", context) { + val coroutineNameRef = CoroutineName(context) + val coroutineIdRef = CoroutineId(context) + val jobRef = Job(context) + val getContextElement: Method = makeMethod("get") + + override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): MirrorOfCoroutineContext? { + val coroutineName = getElementValue(value, context, coroutineNameRef) ?: "coroutine" + val coroutineId = getElementValue(value, context, coroutineIdRef) + val job = getElementValue(value, context, jobRef) + return MirrorOfCoroutineContext(value, coroutineName, coroutineId, job) + } + + fun getElementValue(value: ObjectReference, context: DefaultExecutionContext, keyProvider: ContextKey): T? { + val elementValue = objectValue(value, getContextElement, context, keyProvider.key()) ?: return null + return keyProvider.mirror(elementValue, context) + } +} + +data class MirrorOfCoroutineContext( + val that: ObjectReference, + val name: String, + val id: Long?, + val job: ObjectReference? +) + +abstract class ContextKey(name: String, context: DefaultExecutionContext) : BaseMirror(name, context) { + abstract fun key() : ObjectReference +} + +class CoroutineName(context: DefaultExecutionContext) : ContextKey("kotlinx.coroutines.CoroutineName", context) { + val key = staticObjectValue("Key") + val getNameRef: Method = makeMethod("getName") + + override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): String? { + return stringValue(value, getNameRef, context) + } + + override fun key() = key +} + +class CoroutineId(context: DefaultExecutionContext) : ContextKey("kotlinx.coroutines.CoroutineId", context) { + val key = staticObjectValue("Key") + val getIdRef: Method = makeMethod("getId") + + override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): Long? { + return longValue(value, getIdRef, context) + } + + override fun key() = key +} + +class Job(context: DefaultExecutionContext) : ContextKey("kotlinx.coroutines.Job", context) { + val key = staticObjectValue("Key") + + override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): ObjectReference? { + return value + } + + override fun key() = key +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/mirror/coroutinesMirror.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/mirror/coroutinesMirror.kt new file mode 100644 index 00000000000..e260aaa1b6c --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/mirror/coroutinesMirror.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror + +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.isSubTypeOrSame +import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger +import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext + + +abstract class BaseMirror(val name: String, context: DefaultExecutionContext) { + val log by logger + protected val cls = context.findClass(name) ?: throw IllegalStateException("Can't find class ${name} in remote jvm.") + + fun makeField(fieldName: String): Field = + cls.fieldByName(fieldName) // childContinuation + + fun makeMethod(methodName: String): Method = + cls.methodsByName(methodName).single() + + fun isCompatible(value: ObjectReference) = + value.referenceType().isSubTypeOrSame(name) + + fun mirror(value: ObjectReference, context: DefaultExecutionContext): T? { + if (!isCompatible(value)) { + log.warn("Value ${value.referenceType()} is not compatible with $name.") + return null + } else + return fetchMirror(value, context) + } + + fun staticObjectValue(fieldName: String): ObjectReference { + val keyFieldRef = makeField(fieldName) + return cls.getValue(keyFieldRef) as ObjectReference + } + + fun stringValue(value: ObjectReference, field: Field) = + (value.getValue(field) as StringReference).value() + + fun stringValue(value: ObjectReference, method: Method, context: DefaultExecutionContext) = + (context.invokeMethod(value, method, emptyList()) as StringReference).value() + + fun objectValue(value: ObjectReference, method: Method, context: DefaultExecutionContext, vararg values: Value) = + context.invokeMethodAsObject(value, method, *values) + + fun longValue(value: ObjectReference, method: Method, context: DefaultExecutionContext, vararg values: Value) = + (context.invokeMethodAsObject(value, method, *values) as LongValue).longValue() + + fun objectValue(value: ObjectReference, field: Field) = + value.getValue(field) as ObjectReference + + fun intValue(value: ObjectReference, field: Field) = + (value.getValue(field) as IntegerValue).intValue() + + fun longValue(value: ObjectReference, field: Field) = + (value.getValue(field) as LongValue).longValue() + + protected abstract fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): T? +} + +class StandaloneCoroutine(context: DefaultExecutionContext) : + BaseMirror("kotlinx.coroutines.StandaloneCoroutine", context) { + private val coroutineContextMirror = CoroutineContext(context) + private val childContinuationMirror = ChildContinuation(context) + private val stateFieldRef: Field = makeField("_state") // childContinuation + private val contextFieldRef: Field = makeField("context") + + override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): MirrorOfStandaloneCoroutine { + val state = objectValue(value, stateFieldRef) + val childcontinuation = childContinuationMirror.mirror(state, context) + val cc = objectValue(value, contextFieldRef) + val coroutineContext = coroutineContextMirror.mirror(cc, context) + return MirrorOfStandaloneCoroutine(value, childcontinuation, coroutineContext) + } + +} + +data class MirrorOfStandaloneCoroutine( + val that: ObjectReference, + val state: MirrorOfChildContinuation?, + val context: MirrorOfCoroutineContext? +) + +class ChildContinuation(context: DefaultExecutionContext) : + BaseMirror("kotlinx.coroutines.ChildContinuation", context) { + private val childContinuationMirror = CancellableContinuationImpl(context) + private val childFieldRef: Field = makeField("child") // cancellableContinuationImpl + + override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): MirrorOfChildContinuation? { + val child = objectValue(value, childFieldRef) + return MirrorOfChildContinuation(value, childContinuationMirror.mirror(child, context)) + } +} + +data class MirrorOfChildContinuation( + val that: ObjectReference, + val child: MirrorOfCancellableContinuationImpl? +) + +class CancellableContinuationImpl(context: DefaultExecutionContext) : + BaseMirror("kotlinx.coroutines.CancellableContinuationImpl", context) { + private val coroutineContextMirror = CoroutineContext(context) + private val dispatchedContinuationtMirror = DispatchedContinuation(context) + private val decisionFieldRef: Field = makeField("_decision") + private val delegateFieldRef: Field = makeField("delegate") // DispatchedContinuation + private val resumeModeFieldRef: Field = makeField("resumeMode") + private val submissionTimeFieldRef: Field = makeField("submissionTime") + private val contextFieldRef: Field = makeField("context") + + override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): MirrorOfCancellableContinuationImpl? { + val decision = intValue(value, decisionFieldRef) + val dispatchedContinuation = dispatchedContinuationtMirror.mirror(objectValue(value, delegateFieldRef), context) + val submissionTime = longValue(value, submissionTimeFieldRef) + val resumeMode = intValue(value, resumeModeFieldRef) + val coroutineContext = objectValue(value, contextFieldRef) + val contextMirror = coroutineContextMirror.mirror(coroutineContext, context) + return MirrorOfCancellableContinuationImpl(value, decision, dispatchedContinuation, resumeMode, submissionTime, contextMirror) + } +} + +data class MirrorOfCancellableContinuationImpl( + val that: ObjectReference, + val decision: Int, + val delegate: MirrorOfDispatchedContinuation?, + val resumeMode: Int, + val submissionTyme: Long, + val jobContext: MirrorOfCoroutineContext? +) + +class DispatchedContinuation(context: DefaultExecutionContext) : + BaseMirror("kotlinx.coroutines.DispatchedContinuation", context) { + private val decisionFieldRef: Field = makeField("continuation") + + override fun fetchMirror(value: ObjectReference, context: DefaultExecutionContext): MirrorOfDispatchedContinuation? { + val continuation = objectValue(value, decisionFieldRef) + return MirrorOfDispatchedContinuation(value, continuation) + } +} + +data class MirrorOfDispatchedContinuation( + val that: ObjectReference, + val continuation: ObjectReference?, +) diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/mirror/javaLangMirror.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/mirror/javaLangMirror.kt new file mode 100644 index 00000000000..3a720d20572 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/mirror/javaLangMirror.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror + +import com.sun.jdi.* +import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext + +class JavaLangMirror(context: DefaultExecutionContext) { + // java.lang.Object + val classClsRef = context.findClass("java.lang.Object") as ClassType + val toString: Method = classClsRef.concreteMethodByName("toString", "()Ljava/lang/String;") + + // java.util.List + private val listClsRef = context.findClass("java.util.List") as InterfaceType + private val sizeRef: Method = listClsRef.methodsByName("size").single() + private val getRef: Method = listClsRef.methodsByName("get").single() + + // java.lang.StackTraceElement + private val stackTraceElementClsRef = context.findClass("java.lang.StackTraceElement") as ClassType + private val methodNameFieldRef: Field = stackTraceElementClsRef.fieldByName("methodName") + private val declaringClassFieldRef: Field = stackTraceElementClsRef.fieldByName("declaringClass") + private val fileNameFieldRef: Field = stackTraceElementClsRef.fieldByName("fileName") + private val lineNumberFieldRef: Field = stackTraceElementClsRef.fieldByName("lineNumber") + + // java.lang.Class + val classType = context.findClass("java.lang.Class") as ClassType + + val standaloneCoroutine = StandaloneCoroutine(context) + + + fun string(state: ObjectReference, context: DefaultExecutionContext): String = + (context.invokeMethod(state, toString, emptyList()) as StringReference).value() + + fun elementFromList(instance: ObjectReference, num: Int, context: DefaultExecutionContext) = + context.invokeMethod( + instance, getRef, + listOf(context.vm.virtualMachine.mirrorOf(num)) + ) as ObjectReference + + fun sizeOf(args: ObjectReference, context: DefaultExecutionContext): Int = + (context.invokeMethod(args, sizeRef, emptyList()) as IntegerValue).value() + + fun stackTraceElement(frame: ObjectReference) = + StackTraceElement( + fetchClassName(frame), + fetchMethodName(frame), + fetchFileName(frame), + fetchLine(frame) + ) + + private fun fetchLine(instance: ObjectReference) = + (instance.getValue(lineNumberFieldRef) as? IntegerValue)?.value() ?: -1 + + private fun fetchFileName(instance: ObjectReference) = + (instance.getValue(fileNameFieldRef) as? StringReference)?.value() ?: "" + + private fun fetchMethodName(instance: ObjectReference) = + (instance.getValue(methodNameFieldRef) as? StringReference)?.value() ?: "" + + private fun fetchClassName(instance: ObjectReference) = + (instance.getValue(declaringClassFieldRef) as? StringReference)?.value() ?: "" +} diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/KUtils.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/KUtils.kt index e64a14119a2..94db9914ff8 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/KUtils.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/KUtils.kt @@ -5,9 +5,6 @@ package org.jetbrains.kotlin.idea.debugger.coroutine.util -import com.intellij.debugger.jdi.StackFrameProxyImpl -import com.intellij.debugger.ui.impl.watch.MethodsTracker -import com.intellij.debugger.ui.impl.watch.StackFrameDescriptorImpl import com.intellij.openapi.project.Project import com.intellij.openapi.ui.MessageType import com.intellij.psi.JavaPsiFacade @@ -34,9 +31,6 @@ fun getPosition(stackTraceElement: StackTraceElement, project: Project): XSource return XDebuggerUtil.getInstance().createPosition(classFile, lineNumber) } -class EmptyStackFrameDescriptor(val frame: StackTraceElement, proxy: StackFrameProxyImpl) : - StackFrameDescriptorImpl(proxy, MethodsTracker()) - class ProjectNotification(val project: Project) { fun error(message: String) = XDebuggerManagerImpl.NOTIFICATION_GROUP.createNotification(message, MessageType.ERROR).notify(project) diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/Providers.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/Providers.kt index 38e4d1304fd..f4aa05c6b71 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/Providers.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/Providers.kt @@ -37,4 +37,4 @@ class LoggerDelegate : ReadOnlyProperty { logger = Logger.getInstance(thisRef.javaClass) return logger } -} \ No newline at end of file +} diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineDumpPanel.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineDumpPanel.kt index 36cf25cfbd7..359690212b7 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineDumpPanel.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineDumpPanel.kt @@ -34,6 +34,7 @@ import com.intellij.util.PlatformIcons import com.intellij.util.ui.EmptyIcon import org.jetbrains.kotlin.idea.debugger.coroutine.KotlinDebuggerCoroutinesBundle import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutine.data.State import java.awt.BorderLayout import java.awt.Color import java.awt.datatransfer.StringSelection @@ -143,7 +144,7 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi 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)) { + if (StringUtil.containsIgnoreCase(state.stringStackTrace, text) || StringUtil.containsIgnoreCase(state.key.name, text)) { model.addElement(state) if (selection === state) { selectedIndex = index @@ -180,9 +181,9 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi override fun getData(dataId: String): Any? = if (PlatformDataKeys.EXPORTER_TO_TEXT_FILE.`is`(dataId)) exporterToTextFile else null private fun getCoroutineStateIcon(infoData: CoroutineInfoData): Icon { - return when (infoData.state) { - CoroutineInfoData.State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign) - CoroutineInfoData.State.SUSPENDED -> AllIcons.Actions.Pause + return when (infoData.key.state) { + State.RUNNING -> LayeredIcon(AllIcons.Actions.Resume, Daemon_sign) + State.SUSPENDED -> AllIcons.Actions.Pause else -> EmptyIcon.create(6) } } @@ -190,18 +191,18 @@ class CoroutineDumpPanel(project: Project, consoleView: ConsoleView, toolbarActi private fun getAttributes(infoData: CoroutineInfoData): SimpleTextAttributes { return when { infoData.isSuspended() -> SimpleTextAttributes.GRAY_ATTRIBUTES - infoData.isEmptyStackTrace() -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter()) + infoData.isEmptyStack() -> SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, Color.GRAY.brighter()) else -> SimpleTextAttributes.REGULAR_ATTRIBUTES - } } private inner class CoroutineListCellRenderer : ColoredListCellRenderer() { override fun customizeCellRenderer(list: JList<*>, value: Any, index: Int, selected: Boolean, hasFocus: Boolean) { - val state = value as CoroutineInfoData - icon = getCoroutineStateIcon(state) - val attrs = getAttributes(state) + val infoData = value as CoroutineInfoData + val state = infoData.key + icon = fromState(state.state) + val attrs = getAttributes(infoData) append(state.name + " (", attrs) var detail: String? = state.state.name if (detail == null) { diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineViewDebugSessionListener.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineViewDebugSessionListener.kt index 4f714f30c3d..7bfe98215ae 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineViewDebugSessionListener.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutineViewDebugSessionListener.kt @@ -11,7 +11,6 @@ import com.intellij.xdebugger.XDebugSession import com.intellij.xdebugger.XDebugSessionListener import com.intellij.xdebugger.frame.XSuspendContext import com.intellij.xdebugger.impl.ui.DebuggerUIUtil -import com.sun.jdi.request.EventRequest import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger class CoroutineViewDebugSessionListener( diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/SimpleColoredTextIcon.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/SimpleColoredTextIcon.kt index 20f6e95ca54..9b354bf433f 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/SimpleColoredTextIcon.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/SimpleColoredTextIcon.kt @@ -8,26 +8,21 @@ package org.jetbrains.kotlin.idea.debugger.coroutine.view import com.intellij.debugger.impl.DebuggerUtilsEx import com.intellij.debugger.settings.ThreadsViewSettings import com.intellij.icons.AllIcons -import com.intellij.ide.highlighter.JavaHighlightingColors -import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.icons.AllIcons.General.Information import com.intellij.openapi.editor.HighlighterColors -import com.intellij.openapi.editor.colors.CodeInsightColors -import com.intellij.openapi.editor.colors.EditorColors import com.intellij.openapi.editor.colors.TextAttributesKey -import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.ui.ColoredTextContainer import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes import com.intellij.xdebugger.frame.presentation.XValuePresentation import com.intellij.xdebugger.impl.ui.DebuggerUIUtil import com.intellij.xdebugger.impl.ui.XDebuggerUIConstants -import com.intellij.xdebugger.ui.DebuggerColors import com.sun.jdi.Location import com.sun.jdi.ReferenceType import org.jetbrains.kotlin.idea.debugger.coroutine.KotlinDebuggerCoroutinesBundle import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutine.data.State import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger -import java.awt.Color import javax.swing.Icon class SimpleColoredTextIcon(val icon: Icon?, val hasChildrens: Boolean) { @@ -100,26 +95,31 @@ interface CoroutineDebuggerColors { } } +fun fromState(state: State): Icon = + when (state) { + State.SUSPENDED -> AllIcons.Debugger.ThreadSuspended + State.RUNNING -> AllIcons.Debugger.ThreadRunning + State.CREATED -> AllIcons.Debugger.ThreadStates.Idle + else -> AllIcons.Debugger.ThreadStates.Daemon_sign + } + class SimpleColoredTextIconPresentationRenderer { val log by logger private val settings: ThreadsViewSettings = ThreadsViewSettings.getInstance() fun render(infoData: CoroutineInfoData): SimpleColoredTextIcon { val thread = infoData.activeThread - val name = thread?.name()?.substringBefore(" @${infoData.name}") ?: "" + val name = thread?.name()?.substringBefore(" @${infoData.key.name}") ?: "" val threadState = if (thread != null) DebuggerUtilsEx.getThreadStatusText(thread.status()) else "" - val icon = when (infoData.state) { - CoroutineInfoData.State.SUSPENDED -> AllIcons.Debugger.ThreadSuspended - CoroutineInfoData.State.RUNNING -> AllIcons.Debugger.ThreadRunning - CoroutineInfoData.State.CREATED -> AllIcons.Debugger.ThreadStates.Idle - } + val icon = fromState(infoData.key.state) - val label = SimpleColoredTextIcon(icon, true) + val hasChildren = infoData.stackTrace.isNotEmpty() || infoData.creationStackTrace.isNotEmpty() + val label = SimpleColoredTextIcon(icon, hasChildren) label.append("\"") - label.appendValue(infoData.name) - label.append("\": ${infoData.state}") - if(name.isNotEmpty()) { + label.appendValue(infoData.key.name) + label.append("\": ${infoData.key.state}") + if (name.isNotEmpty()) { label.append(" on thread \"") label.appendValue(name) label.append("\": $threadState") @@ -161,7 +161,7 @@ class SimpleColoredTextIconPresentationRenderer { } else { label.append(name.substring(dotIndex + 1)) if (settings.SHOW_PACKAGE_NAME) { - label.append(" (${name.substring( 0, dotIndex)})") + label.append(" (${name.substring(0, dotIndex)})") } } } @@ -180,15 +180,15 @@ class SimpleColoredTextIconPresentationRenderer { fun renderCreationNode(infoData: CoroutineInfoData) = SimpleColoredTextIcon( AllIcons.Debugger.ThreadSuspended, true, - KotlinDebuggerCoroutinesBundle.message("coroutine.dump.creation.frame", infoData.name) + KotlinDebuggerCoroutinesBundle.message("coroutine.dump.creation.frame", infoData.key.name) ) fun renderErrorNode(error: String) = - SimpleColoredTextIcon(AllIcons.Actions.Lightning,false, error) + SimpleColoredTextIcon(AllIcons.Actions.Lightning,false, KotlinDebuggerCoroutinesBundle.message(error)) - fun renderRoorNode(text: String) = - SimpleColoredTextIcon(null, true, text) + fun renderInfoNode(text: String) = + SimpleColoredTextIcon(AllIcons.General.Information, false, KotlinDebuggerCoroutinesBundle.message(text)) fun renderGroup(groupName: String) = - SimpleColoredTextIcon(AllIcons.Debugger.ThreadGroup,true, groupName) + SimpleColoredTextIcon(AllIcons.Debugger.ThreadGroup, true, groupName) } \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/XCoroutineView.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/XCoroutineView.kt index e6b5c30f808..2fcb82bfd3f 100644 --- a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/XCoroutineView.kt +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/XCoroutineView.kt @@ -6,10 +6,8 @@ package org.jetbrains.kotlin.idea.debugger.coroutine.view import com.intellij.debugger.engine.DebugProcessImpl -import com.intellij.debugger.engine.JavaDebugProcess import com.intellij.debugger.engine.JavaExecutionStack import com.intellij.debugger.engine.SuspendContextImpl -import com.intellij.debugger.jdi.StackFrameProxyImpl import com.intellij.debugger.jdi.ThreadReferenceProxyImpl import com.intellij.ide.CommonActionsManager import com.intellij.openapi.Disposable @@ -19,45 +17,34 @@ import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.search.GlobalSearchScope import com.intellij.ui.CaptionPanel import com.intellij.ui.ComboboxSpeedSearch -import com.intellij.ui.DoubleClickListener import com.intellij.ui.border.CustomLineBorder import com.intellij.ui.components.panels.Wrapper import com.intellij.util.SingleAlarm import com.intellij.xdebugger.XDebugSession -import com.intellij.xdebugger.XDebuggerUtil -import com.intellij.xdebugger.XSourcePosition import com.intellij.xdebugger.frame.* import com.intellij.xdebugger.impl.actions.XDebuggerActions import com.intellij.xdebugger.impl.ui.DebuggerUIUtil -import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreePanel import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeRestorer import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeState import com.intellij.xdebugger.impl.ui.tree.nodes.XValueContainerNode -import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl import com.sun.jdi.request.EventRequest import org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineDebuggerContentInfo import org.jetbrains.kotlin.idea.debugger.coroutine.CoroutineDebuggerContentInfo.Companion.XCOROUTINE_POPUP_ACTION_GROUP import org.jetbrains.kotlin.idea.debugger.coroutine.KotlinDebuggerCoroutinesBundle import org.jetbrains.kotlin.idea.debugger.coroutine.VersionedImplementationProvider -import org.jetbrains.kotlin.idea.debugger.coroutine.data.* -import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ApplicationThreadExecutor +import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineInfoData +import org.jetbrains.kotlin.idea.debugger.coroutine.data.CoroutineStackFrameItem +import org.jetbrains.kotlin.idea.debugger.coroutine.data.CreationCoroutineStackFrameItem import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.CoroutineDebugProbesProxy -import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.LookupContinuation import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ManagerThreadExecutor import org.jetbrains.kotlin.idea.debugger.coroutine.util.CreateContentParams import org.jetbrains.kotlin.idea.debugger.coroutine.util.CreateContentParamsProvider import org.jetbrains.kotlin.idea.debugger.coroutine.util.XDebugSessionListenerProvider import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger -import org.jetbrains.kotlin.idea.debugger.evaluate.DefaultExecutionContext import java.awt.BorderLayout -import java.awt.event.KeyAdapter -import java.awt.event.KeyEvent -import java.awt.event.MouseEvent import javax.swing.JPanel @@ -70,21 +57,20 @@ class XCoroutineView(val project: Project, val session: XDebugSession) : val someCombobox = ComboBox() val panel = XDebuggerTreePanel(project, session.debugProcess.editorsProvider, this, null, XCOROUTINE_POPUP_ACTION_GROUP, null) val alarm = SingleAlarm(Runnable { resetRoot() }, VIEW_CLEAR_DELAY, this) - val javaDebugProcess = session.debugProcess as JavaDebugProcess - val debugProcess: DebugProcessImpl = javaDebugProcess.debuggerSession.process +// val javaDebugProcess = +// val debugProcess: DebugProcessImpl = javaDebugProcess.debuggerSession.process val renderer = SimpleColoredTextIconPresentationRenderer() - val managerThreadExecutor = ManagerThreadExecutor(debugProcess) - val applicationThreadExecutor = ApplicationThreadExecutor() + val managerThreadExecutor = ManagerThreadExecutor(session) var treeState: XDebuggerTreeState? = null private var restorer: XDebuggerTreeRestorer? = null - private var selectedNodeListener = XDebuggerTreeSelectedNodeListener(panel.tree) + private var selectedNodeListener: XDebuggerTreeSelectedNodeListener? = null companion object { private val VIEW_CLEAR_DELAY = 100 //ms } init { - someCombobox.setRenderer(versionedImplementationProvider.comboboxListCellRenderer()) + someCombobox.renderer = versionedImplementationProvider.comboboxListCellRenderer() object : ComboboxSpeedSearch(someCombobox) { override fun getElementText(element: Any?): String? { return element.toString() @@ -93,14 +79,14 @@ class XCoroutineView(val project: Project, val session: XDebugSession) : someCombobox.addItem(null) val myToolbar = createToolbar() val myThreadsPanel = Wrapper() - myThreadsPanel.setBorder(CustomLineBorder(CaptionPanel.CNT_ACTIVE_BORDER_COLOR, 0, 0, 1, 0)) - myThreadsPanel.add(myToolbar?.getComponent(), BorderLayout.EAST) + myThreadsPanel.border = CustomLineBorder(CaptionPanel.CNT_ACTIVE_BORDER_COLOR, 0, 0, 1, 0) + myThreadsPanel.add(myToolbar?.component, BorderLayout.EAST) myThreadsPanel.add(someCombobox, BorderLayout.CENTER) mainPanel.add(panel.mainPanel, BorderLayout.CENTER) - selectedNodeListener.installOn() + selectedNodeListener = XDebuggerTreeSelectedNodeListener(session, panel.tree) + selectedNodeListener?.installOn() } - private fun createToolbar(): ActionToolbarImpl? { val framesGroup = DefaultActionGroup() val actionsManager = CommonActionsManager.getInstance() @@ -127,7 +113,7 @@ class XCoroutineView(val project: Project, val session: XDebugSession) : } } - fun renewRoot(suspendContext: XSuspendContext) { + fun renewRoot(suspendContext: SuspendContextImpl) { panel.tree.setRoot(XCoroutinesRootNode(suspendContext), false) if (treeState != null) { restorer?.dispose() @@ -136,7 +122,10 @@ class XCoroutineView(val project: Project, val session: XDebugSession) : } override fun dispose() { - restorer?.dispose() + if (restorer != null) { + restorer?.dispose() + restorer = null + } } fun forceClear() { @@ -157,15 +146,15 @@ class XCoroutineView(val project: Project, val session: XDebugSession) : inner class EmptyNode : XValueContainerNode(panel.tree, null, true, object : XValueContainer() {}) - inner class XCoroutinesRootNode(suspendContext: XSuspendContext) : + inner class XCoroutinesRootNode(suspendContext: SuspendContextImpl) : XValueContainerNode( panel.tree, null, false, CoroutineGroupContainer(suspendContext, KotlinDebuggerCoroutinesBundle.message("coroutine.view.default.group")) ) - inner class CoroutineGroupContainer(val suspendContext: XSuspendContext, val groupName: String) : XValueContainer() { + inner class CoroutineGroupContainer(val suspendContext: SuspendContextImpl, val groupName: String) : XValueContainer() { override fun computeChildren(node: XCompositeNode) { - if (suspendContext is SuspendContextImpl && suspendContext.suspendPolicy == EventRequest.SUSPEND_ALL) { + if (suspendContext.suspendPolicy == EventRequest.SUSPEND_ALL) { val groups = XValueChildrenList.singleton(CoroutineContainer(suspendContext, groupName)) node.addChildren(groups, true) } else { @@ -178,7 +167,7 @@ class XCoroutineView(val project: Project, val session: XDebugSession) : } inner class CoroutineContainer( - val suspendContext: XSuspendContext, + val suspendContext: SuspendContextImpl, val groupName: String ) : RendererContainer(renderer.renderGroup(groupName)) { @@ -189,38 +178,44 @@ class XCoroutineView(val project: Project, val session: XDebugSession) : var coroutineCache = debugProbesProxy.dumpCoroutines() if (coroutineCache.isOk()) { val children = XValueChildrenList() - coroutineCache.cache.forEach { - children.add(FramesContainer(it, suspendContext)) + for (coroutineInfo in coroutineCache.cache) { + children.add(FramesContainer(coroutineInfo, suspendContext)) } - node.addChildren(children, true) + if (children.size() > 0) + node.addChildren(children, true) + else + node.addChildren(XValueChildrenList.singleton(InfoNode("coroutine.view.fetching.not_found")), true) } else { - val errorNode = ErrorNode(KotlinDebuggerCoroutinesBundle.message("coroutine.view.fetching.error")) + val errorNode = ErrorNode("coroutine.view.fetching.error") node.addChildren(XValueChildrenList.singleton(errorNode), true) } } } } + inner class InfoNode(val error: String) : RendererContainer(renderer.renderInfoNode(error)) + inner class ErrorNode(val error: String) : RendererContainer(renderer.renderErrorNode(error)) inner class FramesContainer( private val infoData: CoroutineInfoData, - private val suspendContext: XSuspendContext + private val suspendContext: SuspendContextImpl ) : RendererContainer(renderer.render(infoData)) { override fun computeChildren(node: XCompositeNode) { managerThreadExecutor.on(suspendContext).schedule { val debugProbesProxy = CoroutineDebugProbesProxy(suspendContext) val children = XValueChildrenList() - debugProbesProxy.frameBuilder().build(infoData) + var stackFrames = debugProbesProxy.frameBuilder().build(infoData) val creationStack = mutableListOf() - infoData.stackFrameList.forEach { - if (it is CreationCoroutineStackFrameItem) - creationStack.add(it) - else if (it is CoroutineStackFrameItem) - children.add(CoroutineFrameValue(it)) + for (frame in stackFrames) { + if (frame is CreationCoroutineStackFrameItem) + creationStack.add(frame) + else if (frame is CoroutineStackFrameItem) + children.add(CoroutineFrameValue(infoData, frame)) } - children.add(CreationFramesContainer(infoData, creationStack)) + if (creationStack.isNotEmpty()) + children.add(CreationFramesContainer(infoData, creationStack)) node.addChildren(children, true) } } @@ -235,17 +230,18 @@ class XCoroutineView(val project: Project, val session: XDebugSession) : val children = XValueChildrenList() creationFrames.forEach { - children.add(CoroutineFrameValue(it)) + children.add(CoroutineFrameValue(infoData, it)) } node.addChildren(children, true) } } inner class CoroutineFrameValue( - val frame: CoroutineStackFrameItem - ) : XNamedValue(frame.uniqueId()) { + val infoData: CoroutineInfoData, + val frameItem: CoroutineStackFrameItem + ) : XNamedValue(frameItem.uniqueId()) { override fun computePresentation(node: XValueNode, place: XValuePlace) = - applyRenderer(node, renderer.render(frame.location)) + applyRenderer(node, renderer.render(frameItem.location)) } private fun applyRenderer(node: XValueNode, presentation: SimpleColoredTextIcon) = @@ -256,137 +252,4 @@ class XCoroutineView(val project: Project, val session: XDebugSession) : applyRenderer(node, presentation) } - data class KeyMouseEvent(val keyEvent: KeyEvent?, val mouseEvent: MouseEvent?) { - constructor(keyEvent: KeyEvent) : this(keyEvent, null) - constructor(mouseEvent: MouseEvent) : this(null, mouseEvent) - - fun isKeyEvent() = keyEvent != null - - fun isMouseEvent() = mouseEvent != null - } - - inner class XDebuggerTreeSelectedNodeListener(val tree: XDebuggerTree) { - - fun installOn() { - object : DoubleClickListener() { - override fun onDoubleClick(e: MouseEvent) = - nodeSelected(KeyMouseEvent(e)) - }.installOn(tree) - - tree.addKeyListener( - object : KeyAdapter() { - override fun keyPressed(e: KeyEvent) { - val key = e.keyCode - if (key == KeyEvent.VK_ENTER || key == KeyEvent.VK_SPACE || key == KeyEvent.VK_RIGHT) - nodeSelected(KeyMouseEvent(e)) - } - }, - ) - } - - fun nodeSelected(event: KeyMouseEvent): Boolean { - val selectedNodes = tree.getSelectedNodes(XValueNodeImpl::class.java, null) - if (selectedNodes.size == 1) { - val node = selectedNodes[0] - val valueContainer = node.valueContainer - if (valueContainer is XCoroutineView.CoroutineFrameValue) { - val frame = valueContainer.frame - val threadSuspendContext = session.suspendContext as SuspendContextImpl - when (frame) { - is RunningCoroutineStackFrameItem -> { - val threadProxy = frame.frame.threadProxy() - val isCurrentContext = threadSuspendContext.thread == threadProxy - createStackAndSetFrame(threadProxy, { frame.stackFrame }, isCurrentContext) - } - is CreationCoroutineStackFrameItem -> { - val position = - getPosition(frame.stackTraceElement.className, frame.stackTraceElement.lineNumber) ?: return false - val threadProxy = threadSuspendContext.thread as ThreadReferenceProxyImpl - createStackAndSetFrame(threadProxy, { SyntheticStackFrame(frame.emptyDescriptor(), emptyList(), position) }) - } - is SuspendCoroutineStackFrameItem -> { - val threadProxy = threadSuspendContext.thread as ThreadReferenceProxyImpl - val executionContext = executionContext(threadSuspendContext, frame.frame) - createStackAndSetFrame(threadProxy, { createSyntheticStackFrame(executionContext, frame) }) - } - is RestoredCoroutineStackFrameItem -> { - val threadProxy = frame.frame.threadProxy() - val position = getPosition(frame.location.declaringType().name(), frame.location.lineNumber()) ?: return false - createStackAndSetFrame( - threadProxy, - { SyntheticStackFrame(frame.emptyDescriptor(), frame.spilledVariables, position) } - ) - - } - else -> { - } - } - } - } - return false - } - } - - fun createStackAndSetFrame( - threadReferenceProxy: ThreadReferenceProxyImpl, - stackFrameProvider: () -> XStackFrame?, - isCurrentContext: Boolean = false - ) { - val threadSuspendContext = session.suspendContext as SuspendContextImpl - managerThreadExecutor.on(threadSuspendContext).schedule { - val stackFrame = stackFrameProvider.invoke() - if (stackFrame is XStackFrame) { - val executionStack = createExecutionStack(threadReferenceProxy, isCurrentContext) - applicationThreadExecutor.schedule( - { - session.setCurrentStackFrame(executionStack, stackFrame) - }, - panel.tree - ) - } - } - } - - private fun createExecutionStack(proxy: ThreadReferenceProxyImpl, isCurrentContext: Boolean = false): XExecutionStack { - val executionStack = CoroutineDebuggerExecutionStack(proxy, isCurrentContext) - executionStack.initTopFrame() - return executionStack - } - - inner class CoroutineDebuggerExecutionStack(threadReferenceProxy: ThreadReferenceProxyImpl, isCurrentContext: Boolean) : - JavaExecutionStack(threadReferenceProxy, debugProcess, isCurrentContext) - - - private fun getPosition(className: String, lineNumber: Int): XSourcePosition? { - val psiFacade = JavaPsiFacade.getInstance(project) - val psiClass = psiFacade.findClass( - className.substringBefore("$"), // find outer class, for which psi exists TODO - GlobalSearchScope.everythingScope(project) - ) - val classFile = psiClass?.containingFile?.virtualFile - // to convert to 0-based line number or '-1' to do not move - val lineNumber = if (lineNumber > 0) lineNumber - 1 else return null - return XDebuggerUtil.getInstance().createPosition(classFile, lineNumber) - } - - private fun executionContext(suspendContext: SuspendContextImpl, frameProxy: StackFrameProxyImpl): DefaultExecutionContext { - return DefaultExecutionContext(suspendContext, frameProxy) - } - - private fun createSyntheticStackFrame( - executionContext: DefaultExecutionContext, - frame: SuspendCoroutineStackFrameItem - ): SyntheticStackFrame? { - - val position = - applicationThreadExecutor.readAction { getPosition(frame.stackTraceElement.className, frame.stackTraceElement.lineNumber) } - ?: return null - val lookupContinuation = LookupContinuation(executionContext, frame.stackTraceElement) - val continuation = lookupContinuation.findContinuation(frame.lastObservedFrameFieldRef) ?: return null - - val asyncStackTraceContext = lookupContinuation.createAsyncStackTraceContext(continuation) - val vars = asyncStackTraceContext?.getSpilledVariables(continuation) ?: return null - return SyntheticStackFrame(frame.emptyDescriptor(), vars, position) - } } - diff --git a/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/XDebuggerTreeSelectedNodeListener.kt b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/XDebuggerTreeSelectedNodeListener.kt new file mode 100644 index 00000000000..cd9b199bb05 --- /dev/null +++ b/idea/jvm-debugger/jvm-debugger-coroutine/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/XDebuggerTreeSelectedNodeListener.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.debugger.coroutine.view + +import com.intellij.debugger.engine.DebugProcessImpl +import com.intellij.debugger.engine.JavaDebugProcess +import com.intellij.debugger.engine.JavaExecutionStack +import com.intellij.debugger.engine.SuspendContextImpl +import com.intellij.debugger.jdi.StackFrameProxyImpl +import com.intellij.debugger.jdi.ThreadReferenceProxyImpl +import com.intellij.ui.DoubleClickListener +import com.intellij.xdebugger.XDebugSession +import com.intellij.xdebugger.frame.XExecutionStack +import com.intellij.xdebugger.frame.XStackFrame +import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree +import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl +import com.sun.jdi.ObjectReference +import org.jetbrains.kotlin.idea.debugger.coroutine.data.* +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ApplicationThreadExecutor +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.ContinuationHolder +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.findPosition +import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.suspendContextImpl +import org.jetbrains.kotlin.idea.debugger.invokeInManagerThread +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import java.awt.event.MouseEvent + +class XDebuggerTreeSelectedNodeListener(val session: XDebugSession, val tree: XDebuggerTree) { + val applicationThreadExecutor = ApplicationThreadExecutor() + val javaDebugProcess = session.debugProcess as JavaDebugProcess + val debugProcess: DebugProcessImpl = javaDebugProcess.debuggerSession.process + + fun installOn() { + object : DoubleClickListener() { + override fun onDoubleClick(e: MouseEvent) = + nodeSelected(KeyMouseEvent(e)) + }.installOn(tree) + + tree.addKeyListener( + object : KeyAdapter() { + override fun keyPressed(e: KeyEvent) { + val key = e.keyCode + if (key == KeyEvent.VK_ENTER || key == KeyEvent.VK_SPACE || key == KeyEvent.VK_RIGHT) + nodeSelected(KeyMouseEvent(e)) + } + }, + ) + } + + fun nodeSelected(event: KeyMouseEvent): Boolean { + val selectedNodes = tree.getSelectedNodes(XValueNodeImpl::class.java, null) + if (selectedNodes.size == 1) { + val node = selectedNodes[0] + val valueContainer = node.valueContainer + val suspendContext = session.suspendContextImpl() + if (valueContainer is XCoroutineView.CoroutineFrameValue) { + when (val stackFrameItem = valueContainer.frameItem) { + is RunningCoroutineStackFrameItem -> { + val threadProxy = stackFrameItem.frame.threadProxy() + val isCurrentContext = suspendContext.thread == threadProxy + val executionStack = JavaExecutionStack( + threadProxy, + debugProcess, + isCurrentContext + ) + val jStackFrame = executionStack.createStackFrame(stackFrameItem.frame) + createStackAndSetFrame(threadProxy, { jStackFrame }, isCurrentContext) + } + is CreationCoroutineStackFrameItem -> { + val position = stackFrameItem.stackTraceElement.findPosition(session.project) ?: return false + val threadProxy = suspendContext.thread ?: return false + val realFrame = threadProxy.forceFrames().first() ?: return false + createStackAndSetFrame(threadProxy, { + SyntheticStackFrame(stackFrameItem.emptyDescriptor(realFrame), emptyList(), position) + }) + } + is SuspendCoroutineStackFrameItem -> { + val threadProxy = suspendContext.thread ?: return false + val realFrame = threadProxy.forceFrames().first() ?: return false + val lastFrame = valueContainer.infoData.lastObservedFrameFieldRef ?: return false + createStackAndSetFrame(threadProxy, { createSyntheticStackFrame(suspendContext, stackFrameItem, realFrame, lastFrame) }) + } + is RestoredCoroutineStackFrameItem -> { + val threadProxy = stackFrameItem.frame.threadProxy() + val position = stackFrameItem.location.findPosition(session.project) + ?: return false + createStackAndSetFrame(threadProxy, { + SyntheticStackFrame(stackFrameItem.emptyDescriptor(), stackFrameItem.spilledVariables, position) + }) + } + is DefaultCoroutineStackFrameItem -> { + val threadProxy = suspendContext.thread ?: return false + val position = stackFrameItem.location.findPosition(session.project) + ?: return false + val realFrame = threadProxy.forceFrames().first() ?: return false + createStackAndSetFrame(threadProxy, { + SyntheticStackFrame(stackFrameItem.emptyDescriptor(realFrame), stackFrameItem.spilledVariables, position) + }) + } + else -> { + } + } + } + } + return false + } + + fun createStackAndSetFrame( + threadReferenceProxy: ThreadReferenceProxyImpl, + stackFrameProvider: () -> XStackFrame?, + isCurrentContext: Boolean = false + ) { + val stackFrameStack = debugProcess.invokeInManagerThread { + val stackFrame = stackFrameProvider.invoke() ?: return@invokeInManagerThread null + XStackFrameStack(stackFrame, createExecutionStack(threadReferenceProxy, isCurrentContext)) + } ?: return + setCurrentStackFrame(stackFrameStack) + } + + fun setCurrentStackFrame(stackFrameStack: XStackFrameStack) { + applicationThreadExecutor.schedule( + { + session.setCurrentStackFrame(stackFrameStack.executionStack, stackFrameStack.stackFrame) + }, tree + ) + } + + data class XStackFrameStack(val stackFrame: XStackFrame, val executionStack: XExecutionStack); + + private fun createExecutionStack(proxy: ThreadReferenceProxyImpl, isCurrentContext: Boolean = false): XExecutionStack { + val executionStack = JavaExecutionStack(proxy, debugProcess, isCurrentContext) + executionStack.initTopFrame() + return executionStack + } + + + private fun createSyntheticStackFrame( + suspendContext: SuspendContextImpl, + frame: SuspendCoroutineStackFrameItem, + topFrame: StackFrameProxyImpl, + initialContinuation: ObjectReference + ): SyntheticStackFrame? { + val position = + applicationThreadExecutor.readAction { frame.stackTraceElement.findPosition(session.project) } + ?: return null + val continuation = + ContinuationHolder.lookup(suspendContext, initialContinuation) + ?: return null + + return SyntheticStackFrame( + frame.emptyDescriptor(topFrame), + continuation.getSpilledVariables() ?: return null, + position + ) + } +} + +data class KeyMouseEvent(val keyEvent: KeyEvent?, val mouseEvent: MouseEvent?) { + constructor(keyEvent: KeyEvent) : this(keyEvent, null) + constructor(mouseEvent: MouseEvent) : this(null, mouseEvent) + + fun isKeyEvent() = keyEvent != null + + fun isMouseEvent() = mouseEvent != null +} \ No newline at end of file diff --git a/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractAsyncStackTraceTest.kt b/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractAsyncStackTraceTest.kt index 0dcb668ed39..49852b6eff1 100644 --- a/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractAsyncStackTraceTest.kt +++ b/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractAsyncStackTraceTest.kt @@ -6,6 +6,7 @@ package org.jetbrains.kotlin.idea.debugger.test import com.intellij.debugger.engine.AsyncStackTraceProvider +import com.intellij.debugger.engine.JavaStackFrame import com.intellij.debugger.engine.JavaValue import com.intellij.debugger.memory.utils.StackFrameItem import com.intellij.execution.process.ProcessOutputTypes @@ -37,7 +38,7 @@ abstract class AbstractAsyncStackTraceTest : KotlinDescriptorTestCaseWithSteppin val frameProxy = this.frameProxy if (frameProxy != null) { try { - val stackTrace = asyncStackTraceProvider.getAsyncStackTraceSafe(frameProxy, this) + val stackTrace = asyncStackTraceProvider.lookupForResumeContinuation(frameProxy, this) if (stackTrace != null && stackTrace.isNotEmpty()) { print(renderAsyncStackTrace(stackTrace), ProcessOutputTypes.SYSTEM) } else { diff --git a/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractCoroutineDumpTest.kt b/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractCoroutineDumpTest.kt index 1d96c1d1774..9953f4111a1 100644 --- a/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractCoroutineDumpTest.kt +++ b/idea/jvm-debugger/jvm-debugger-test/test/org/jetbrains/kotlin/idea/debugger/test/AbstractCoroutineDumpTest.kt @@ -52,7 +52,7 @@ abstract class AbstractCoroutineDumpTest : KotlinDescriptorTestCaseWithStepping( private fun stringDump(infoData: List) = buildString { infoData.forEach { - appendln("\"${it.name}\", state: ${it.state}") + appendln("\"${it.key.name}\", state: ${it.key.state}") } } diff --git a/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/evaluate/ExecutionContext.kt b/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/evaluate/ExecutionContext.kt index b64437eb1f2..fc6fb8c68b4 100644 --- a/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/evaluate/ExecutionContext.kt +++ b/idea/jvm-debugger/jvm-debugger-util/src/org/jetbrains/kotlin/idea/debugger/evaluate/ExecutionContext.kt @@ -194,6 +194,7 @@ sealed class BaseExecutionContext(val evaluationContext: EvaluationContextImpl) fun findAndInvoke(instance: ObjectReference, name: String, methodSignature: String? = null, vararg params: Value): Value? { val type = instance.referenceType() + type.allMethods() val method = if (methodSignature is String) type.methodsByName(name, methodSignature).single() diff --git a/idea/resources/META-INF/jvm.xml b/idea/resources/META-INF/jvm.xml index 2f357e7b0a4..4cff3b894ad 100644 --- a/idea/resources/META-INF/jvm.xml +++ b/idea/resources/META-INF/jvm.xml @@ -189,10 +189,22 @@ description="Enable bytecode instrumentation for Kotlin classes" defaultValue="false" restartRequired="false"/> - + + + diff --git a/idea/resources/META-INF/plugin-common.xml b/idea/resources/META-INF/plugin-common.xml index 64e1456101e..bf5ab6a07c7 100644 --- a/idea/resources/META-INF/plugin-common.xml +++ b/idea/resources/META-INF/plugin-common.xml @@ -380,6 +380,9 @@ + +