diff --git a/integration-testing/build.gradle.kts b/integration-testing/build.gradle.kts index b84cbc453b..9198c5cf22 100644 --- a/integration-testing/build.gradle.kts +++ b/integration-testing/build.gradle.kts @@ -233,7 +233,8 @@ tasks { ":jpmsTest:check", "smokeTest:build", "java8Test:check", - "safeDebugAgentTest:runWithExpectedFailure" + "safeDebugAgentTest:runWithExpectedFailure", + "safeDebugAgentTest:runWithIgnoredError" ) } diff --git a/integration-testing/safeDebugAgentTest/build.gradle.kts b/integration-testing/safeDebugAgentTest/build.gradle.kts index a4e010ba21..c285b6bc09 100644 --- a/integration-testing/safeDebugAgentTest/build.gradle.kts +++ b/integration-testing/safeDebugAgentTest/build.gradle.kts @@ -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("runWithExpectedFailure") { +tasks.register("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("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) + } } -} \ No newline at end of file +} diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/AgentPremain.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/AgentPremain.kt index 92786c795d..9e50f0feb3 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/AgentPremain.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/AgentPremain.kt @@ -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 @@ -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() @@ -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) + } + } + } +}