Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion integration-testing/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ tasks {
":jpmsTest:check",
"smokeTest:build",
"java8Test:check",
"safeDebugAgentTest:runWithExpectedFailure"
"safeDebugAgentTest:runWithExpectedFailure",
"safeDebugAgentTest:runWithIgnoredError"
)
}

Expand Down
79 changes: 59 additions & 20 deletions integration-testing/safeDebugAgentTest/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,70 @@ application {
mainClass.set("Main")
}

val expectedAgentError =
"kotlinx.coroutines debug agent failed to load.\n" +
"Please ensure that the Kotlin standard library is present in the classpath.\n" +
"Alternatively, you can disable kotlinx.coroutines debug agent by removing `-javaagent=/path/kotlinx-coroutines-core.jar` from your VM arguments.\n"


// In this test coroutine debug agent is attached as a javaagent vm argument
// to a pure Java project (safeDebugAgentTest) with no Kotlin stdlib dependency.
// In this case the error should be thrown from AgetnPremain class:
// "java.lang.IllegalStateException: kotlinx.coroutines debug agent failed to load."
tasks.register<Test>("runWithExpectedFailure") {
tasks.register<Task>("runWithExpectedFailure") {
dependsOn("compileJava")
val agentJar = System.getProperty("coroutines.debug.agent.path")
val errorOutputStream = ByteArrayOutputStream()
val standardOutputStream = ByteArrayOutputStream()

project.javaexec {
mainClass.set("Main")
classpath = sourceSets.main.get().runtimeClasspath
jvmArgs = listOf("-javaagent:$agentJar")
errorOutput = errorOutputStream
standardOutput = standardOutputStream
isIgnoreExitValue = true

doFirst {
val errorOutputStream = ByteArrayOutputStream()
val standardOutputStream = ByteArrayOutputStream()

project.javaexec {
mainClass.set("Main")
classpath = sourceSets.main.get().runtimeClasspath
jvmArgs = listOf("-javaagent:$agentJar")
errorOutput = errorOutputStream
standardOutput = standardOutputStream
isIgnoreExitValue = true
}

val errorOutput = errorOutputStream.toString()
val standardOutput = standardOutputStream.toString()
if (!errorOutput.contains(expectedAgentError)) {
throw GradleException("':safeDebugAgentTest:runWithExpectedFailure' completed with an unexpected output:\n" + standardOutput + "\n" + errorOutput)
}
if (standardOutput.contains("OK!")) {
throw GradleException("':safeDebugAgentTest:runWithExpectedFailure' was expected to throw the agent initializaion error, but Main was executed:\n" + standardOutput + "\n" + errorOutput)
}
}
}

val expectedAgentError =
"kotlinx.coroutines debug agent failed to load.\n" +
"Please ensure that the Kotlin standard library is present in the classpath.\n" +
"Alternatively, you can disable kotlinx.coroutines debug agent by removing `-javaagent=/path/kotlinx-coroutines-core.jar` from your VM arguments.\n"
val errorOutput = errorOutputStream.toString()
val standardOutput = standardOutputStream.toString()
if (!errorOutput.contains(expectedAgentError)) {
throw GradleException("':safeDebugAgentTest:runWithExpectedFailure' completed with an unexpected output:\n" + standardOutput + "\n" + errorOutput)
// This test checks, that if the argument `kotlinx.coroutines.ignore.debug.agent.error` is passed to the javaagent,
// then the initialization error will be just logged to the stderr and the execution will continue.
tasks.register<Task>("runWithIgnoredError") {
dependsOn("compileJava")
val agentJar = System.getProperty("coroutines.debug.agent.path")

doFirst {
val errorOutputStream = ByteArrayOutputStream()
val standardOutputStream = ByteArrayOutputStream()

project.javaexec {
mainClass.set("Main")
classpath = sourceSets.main.get().runtimeClasspath
jvmArgs = listOf("-javaagent:$agentJar=kotlinx.coroutines.ignore.debug.agent.error")
errorOutput = errorOutputStream
standardOutput = standardOutputStream
isIgnoreExitValue = true
}

val errorOutput = errorOutputStream.toString()
val standardOutput = standardOutputStream.toString()
if (!errorOutput.contains(expectedAgentError)) {
throw GradleException("':safeDebugAgentTest:runWithIgnoredError' completed with an unexpected output:\n" + standardOutput + "\n" + errorOutput)
}
if (!standardOutput.contains("OK!")) {
throw GradleException("':safeDebugAgentTest:runWithIgnoredError' was expected to log the agent initialization error and proceed with the execution of Main, but it completed with an unexpected output:\n" + standardOutput + "\n" + errorOutput)
}
}
}
}
57 changes: 37 additions & 20 deletions kotlinx-coroutines-core/jvm/src/debug/internal/AgentPremain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package kotlinx.coroutines.debug.internal
import android.annotation.*
import org.codehaus.mojo.animal_sniffer.*
import sun.misc.*
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.IllegalStateException
import java.lang.instrument.*
import java.lang.instrument.ClassFileTransformer
Expand All @@ -15,26 +17,7 @@ import java.security.*
@Suppress("unused")
@SuppressLint("all")
@IgnoreJRERequirement // Never touched on Android
internal object AgentPremain {

// This should be the first property to ensure the check happens first! Add new properties only below!
private val dummy = checkIfStdlibIsAvailable()

/**
* This check ensures that kotlin-stdlib classes are loaded by the time AgentPremain is initialized;
* otherwise the debug session would fail with `java.lang.NoClassDefFoundError: kotlin/Result`.
*/
private fun checkIfStdlibIsAvailable() {
try {
Result.success(42)
} catch (t: Throwable) {
throw IllegalStateException("kotlinx.coroutines debug agent failed to load.\n" +
"Please ensure that the Kotlin standard library is present in the classpath.\n" +
"Alternatively, you can disable kotlinx.coroutines debug agent by removing `-javaagent=/path/kotlinx-coroutines-core.jar` from your VM arguments.\n" +
t.cause
)
}
}
internal object AgentPremainImpl {

private val enableCreationStackTraces = runCatching {
System.getProperty("kotlinx.coroutines.debug.enable.creation.stack.trace")?.toBoolean()
Expand Down Expand Up @@ -89,3 +72,37 @@ internal object AgentPremain {
}
}
}

/**
* This class serves as a "safe" wrapper around [AgentPremainImpl] that does not require any kotlin-stdlib classes to be loaded for its initialization.
* It may throw an error containing the cause of the agent initialization failure or just log the error to the error output.
*
* By default, if agent initialization fails, e.g., if kotlin-stdlib is not found in the classpath,
* the agent will throw an [IllegalStateException].
*
* If `kotlinx.coroutines.ignore.debug.agent.error` is passed as an argument to the debug agent
* (like this: `-javaagent:/path/kotlinx-coroutines-core.jar=kotlinx.coroutines.ignore.debug.agent.error`),
* then the initialization error will be logged to stderr, the agent will not be attached, but the execution will continue.
*/
internal object AgentPremain {
@JvmStatic
@Suppress("UNUSED_PARAMETER")
fun premain(args: String?, instrumentation: Instrumentation) {
val shouldIgnoreError = (args as? java.lang.String)?.contains("kotlinx.coroutines.ignore.debug.agent.error") ?: false
try {
AgentPremainImpl.premain(args, instrumentation)
} catch (t: Throwable) {
val sw = StringWriter()
t.printStackTrace(PrintWriter(sw))
val errorMessage = "kotlinx.coroutines debug agent failed to load.\n" +
"Please ensure that the Kotlin standard library is present in the classpath.\n" +
"Alternatively, you can disable kotlinx.coroutines debug agent by removing `-javaagent=/path/kotlinx-coroutines-core.jar` from your VM arguments.\n" +
sw.toString()
if (shouldIgnoreError) {
System.err.println(errorMessage)
} else {
throw IllegalStateException( errorMessage)
}
}
}
}