diff --git a/integration-testing/README.md b/integration-testing/README.md index deed97b35a..f3342dbe8e 100644 --- a/integration-testing/README.md +++ b/integration-testing/README.md @@ -9,6 +9,7 @@ The tests are the following: * `debugAgentTest` checks that the coroutine debugger can be run as a Java agent. * `debugDynamicAgentTest` checks that `kotlinx-coroutines-debug` agent can self-attach dynamically to JVM as a standalone dependency. * `debugDynamicAgentJpmsTest` checks that `kotlinx-coroutines-debug` agent can self-attach dynamically to JVM as a standalone dependency (with JPMS) +* `externalStaticDebugProbesTest` checks that a `ExternalStaticDebugProbes` is picked up by the kotlin stdlib * `smokeTest` builds the multiplatform test project that depends on coroutines. The `integration-testing` project is expected to be in a subdirectory of the main `kotlinx.coroutines` project. diff --git a/integration-testing/jpmsTest/build.gradle.kts b/integration-testing/jpmsTest/build.gradle.kts index f96f99822f..80027e3d06 100644 --- a/integration-testing/jpmsTest/build.gradle.kts +++ b/integration-testing/jpmsTest/build.gradle.kts @@ -21,6 +21,7 @@ kotlin { jvmToolchain(17) val test = target.compilations.getByName("test") + target.compilations.create("debugDynamicAgentJpmsTest") { associateWith(test) @@ -35,6 +36,22 @@ kotlin { classpath = javaSourceSet.runtimeClasspath } } + + + target.compilations.create("externalStaticDebugProbesTest") { + associateWith(test) + + + defaultSourceSet.dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-debug:$coroutines_version") + } + + tasks.register("externalStaticDebugProbesTest") { + testClassesDirs = output.classesDirs + classpath = javaSourceSet.runtimeClasspath + } + } } tasks.named("check") { diff --git a/integration-testing/jpmsTest/src/externalStaticDebugProbesTest/kotlin/ExternalStaticDebugProbes.kt b/integration-testing/jpmsTest/src/externalStaticDebugProbesTest/kotlin/ExternalStaticDebugProbes.kt new file mode 100644 index 0000000000..deab9ea6ce --- /dev/null +++ b/integration-testing/jpmsTest/src/externalStaticDebugProbesTest/kotlin/ExternalStaticDebugProbes.kt @@ -0,0 +1,10 @@ +package kotlinx.coroutines.external + +import kotlinx.coroutines.debug.internal.AbstractStaticDebugProbes +import kotlin.coroutines.* + +object ExternalStaticDebugProbes: AbstractStaticDebugProbes() { + override fun probeCoroutineCreated(completion: Continuation): Continuation { + return super.probeCoroutineCreated(completion) + } +} \ No newline at end of file diff --git a/integration-testing/jpmsTest/src/externalStaticDebugProbesTest/kotlin/ExternalStaticDebugProbesTest.kt b/integration-testing/jpmsTest/src/externalStaticDebugProbesTest/kotlin/ExternalStaticDebugProbesTest.kt new file mode 100644 index 0000000000..986a416bad --- /dev/null +++ b/integration-testing/jpmsTest/src/externalStaticDebugProbesTest/kotlin/ExternalStaticDebugProbesTest.kt @@ -0,0 +1,20 @@ +import org.junit.* +import kotlinx.coroutines.* +import kotlinx.coroutines.external.ExternalStaticDebugProbes +import org.junit.Test +import java.io.* + +class ExternalStaticDebugProbesTest { + + @Test + fun testDumpCoroutines() { + runBlocking { + val baos = ByteArrayOutputStream() + ExternalStaticDebugProbes.dumpCoroutines(PrintStream(baos)) + // if the agent works, then dumps should contain something, + // at least the fact that this test is running. + val dump = baos.toString() + Assert.assertTrue(dump, dump.contains("testDumpCoroutines")) + } + } +} diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 91cccb7bd1..ac6e6462ff 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -911,6 +911,14 @@ public final class kotlinx/coroutines/channels/TickerMode : java/lang/Enum { public static fun values ()[Lkotlinx/coroutines/channels/TickerMode; } +public abstract class kotlinx/coroutines/debug/internal/AbstractStaticDebugProbes { + public fun ()V + public final fun dumpCoroutines (Ljava/io/PrintStream;)V + public fun probeCoroutineCreated (Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; + public fun probeCoroutineResumed (Lkotlin/coroutines/Continuation;)V + public fun probeCoroutineSuspended (Lkotlin/coroutines/Continuation;)V +} + public final class kotlinx/coroutines/debug/internal/AgentInstallationType { public static final field INSTANCE Lkotlinx/coroutines/debug/internal/AgentInstallationType; } diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/AbstractStaticDebugProbes.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/AbstractStaticDebugProbes.kt new file mode 100644 index 0000000000..7fe883d416 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/AbstractStaticDebugProbes.kt @@ -0,0 +1,59 @@ +package kotlinx.coroutines.debug.internal + +import kotlinx.coroutines.* +import java.io.* +import kotlin.coroutines.* + +/** + * Allows to statically install 'Debug Probes' at the known location + * (kotlinx.coroutines.external.ExternalStaticDebugProbes). + * + * **Discussion** + * + * There are three methods of installing/engaging coroutines 'Debug Probes' + * + * 1) Dynamic Attach (using the 'kotlinx-coroutines-debug' module) + * This uses runtime byte-code alteration to replace the 'Debug Probes' straight from the kotlin-stdlib + * + * 2) Static Attach using an Agent + * This uses a java agent to replace the 'Debug Probes' from the kotlin-stdlib statically + * + * 3) ExternalStaticDebugProbes + * The kotlin-stdlib compiled against a class at + * `kotlinx.coroutines.external.ExternalStaticDebugProbes` which is not available at runtime, by default. + * If a class at this location is present, then the kotlin-stdlib will call into it. + * + * ```kotlin + * package kotlinx.coroutines.external + * object ExternalStaticDebugProbes: AbstractStaticDebugProbes() { + * override fun probeCoroutineCreated(completion: Continuation): Continuation { + * // intercept + * // ... + * + * // forward to debugger machinery + * return super.probeCoroutineCreated(completion) + * } + * } + * ``` + */ +@Suppress("unused") +@DelicateCoroutinesApi +@ExperimentalCoroutinesApi +abstract class AbstractStaticDebugProbes { + init { + require(javaClass.name == "kotlinx.coroutines.external.ExternalStaticDebugProbes") + AgentInstallationType.isInstalledStatically = true + DebugProbesImpl.install() + } + + open fun probeCoroutineResumed(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineResumed(frame) + + open fun probeCoroutineSuspended(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineSuspended(frame) + + open fun probeCoroutineCreated(completion: Continuation): Continuation = + DebugProbesImpl.probeCoroutineCreated(completion) + + fun dumpCoroutines(out: PrintStream) { + DebugProbesImpl.dumpCoroutines(out) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfoImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfoImpl.kt index 47d69363c8..3ef8f2fe8e 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfoImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugCoroutineInfoImpl.kt @@ -11,6 +11,8 @@ internal const val SUSPENDED = "SUSPENDED" /** * Internal implementation class where debugger tracks details it knows about each coroutine. * Its mutable fields can be updated concurrently, thus marked with `@Volatile` + * + * Usage Note: IntelliJ/Coroutines Debugger: Reflection */ @PublishedApi internal class DebugCoroutineInfoImpl internal constructor( diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index 25594ad01a..9b7ab124c7 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -14,6 +14,16 @@ import kotlin.coroutines.jvm.internal.CoroutineStackFrame import kotlin.synchronized import _COROUTINE.ArtificialStackFrames +/** + * Usage Note: IntelliJ @SuppressWarnings({"KotlinInternalInJava"}): CoroutineDumpState + * call to 'install' + * + * Usage Note: IntelliJ @SuppressWarnings({"KotlinInternalInJava"}): DebugProbesKt + * Custom 'DebugProbesKt' class providing 'probeCoroutineCreated', 'probeCoroutineResumed', 'probeCoroutineSuspended' + * calling into DebugProbesImpl (similar to our DebugProbesKt) + * + * Usage Note: IntelliJ/Coroutines Debugger: Reflection + */ @PublishedApi internal object DebugProbesImpl { private val ARTIFICIAL_FRAME = ArtificialStackFrames().coroutineCreation() @@ -527,6 +537,8 @@ internal object DebugProbesImpl { /** * This class is injected as completion of all continuations in [probeCoroutineCompleted]. * It is owning the coroutine info and responsible for managing all its external info related to debug agent. + * + * Usage Note: IntelliJ/Coroutines Debugger: Reflection */ public class CoroutineOwner internal constructor( @JvmField internal val delegate: Continuation,