Skip to content

Ensure the coroutines debug agent attaches even with no stdlib#4588

Merged
dkhalanskyjb merged 2 commits into
developfrom
dkhalanskyjb/prevent-debug-agent-failures-on-no-stdlib
Mar 5, 2026
Merged

Ensure the coroutines debug agent attaches even with no stdlib#4588
dkhalanskyjb merged 2 commits into
developfrom
dkhalanskyjb/prevent-debug-agent-failures-on-no-stdlib

Conversation

@dkhalanskyjb

Copy link
Copy Markdown
Collaborator

Alternative for #4565

With this change, we ensure that AgentPremain does not touch any of the kotlin.* APIs when it's being attached to a process. This means that even if it's attached by a mistake to a pure Java process, it won't cause any harm.

Most of the changes are limited to removing the runCatching antipattern, but one sad consequence is the inability to use the convenient thread { } function.

An integration test was implemented, checking that a pure Java process completes successfully even with the debug agent attached to it.
The earlier version of the test performed the check even before the Java class got compiled. It was fine, given that the execution never even got to the main class and crashed during the debug agent attach. With the proposed change, however, it had to be significantly modified.

@mvicsokolova

Copy link
Copy Markdown
Contributor

Thank you! I've tested this change on the debugger side

@fzhinkin fzhinkin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build scripts could be improved to provide more context if the test will fail, but otherwise the change looks good.

Comment on lines +22 to +39
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
}

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)
check(errorOutputStream.toString().isEmpty()) {
"Expected no output in the error stream, but got:\n$errorOutputStream"
}
check(standardOutputStream.toString().contains("OK!")) {
"Expected 'OK!' in the standard output, but got:\n$standardOutputStream"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
}
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)
check(errorOutputStream.toString().isEmpty()) {
"Expected no output in the error stream, but got:\n$errorOutputStream"
}
check(standardOutputStream.toString().contains("OK!")) {
"Expected 'OK!' in the standard output, but got:\n$standardOutputStream"
}
val agentJar = System.getProperty("coroutines.debug.agent.path")
val execResult = project.providers.javaexec {
mainClass.set("Main")
classpath = sourceSets.main.get().runtimeClasspath
jvmArgs = listOf("-javaagent:$agentJar")
isIgnoreExitValue = true
}
val exitCode = execResult.result.get().exitValue
val stdout = execResult.standardOutput.asText.getOrElse("<not found>")
val stderr = execResult.standardError.asText.getOrElse("<not found>")
check (exitCode == 0) {
"Process execution ended with non-zero exit code: $exitCode\nstdout:\n$stdout\nstderr:\n$stderr"
}
check(stderr.isEmpty()) {
"Expected no output in the error stream, but got:\n$stderr"
}
check(stdout.contains("OK!")) {
"Expected 'OK!' in the standard output, but got:\n$stdout"
}

There were a few issues:

  • Project.javaexec is deprecated and it's better to use a supported alternative
  • With original javaexec settings, any non-zero exit code would result in an exception with "non-zero exit code" as a result, but without any details and without stdout/err contents.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied the suggestion. Github's "Commit suggestion" refused to do that, saying that it can't apply suggestions to removed lines.

With this change, we ensure that `AgentPremain` does not touch
any of the `kotlin.*` APIs when it's being attached to a process.
This means that even if it's attached by a mistake to a pure Java
process, it won't cause any harm.

Most of the changes are limited to removing non-idiomatic
`runCatching` calls, but one sad consequence is the inability
to use the convenient `thread { }` function.

An integration test was implemented checking that a
pure Java process completes successfully even with the debug agent
attached to it.
The earlier version of the test performed the check even before
the Java class got compiled, which was fine, given that
the execution never even got to the main class and crashed during
the debug agent attach, so it had to be significantly changed.
@dkhalanskyjb dkhalanskyjb force-pushed the dkhalanskyjb/prevent-debug-agent-failures-on-no-stdlib branch from ef2b842 to 3340c6a Compare March 4, 2026 09:20
@dkhalanskyjb dkhalanskyjb requested a review from fzhinkin March 4, 2026 09:21
@dkhalanskyjb dkhalanskyjb merged commit 55802b4 into develop Mar 5, 2026
1 check passed
@dkhalanskyjb dkhalanskyjb deleted the dkhalanskyjb/prevent-debug-agent-failures-on-no-stdlib branch March 5, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants