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 @@ + +