Skip to content

Commit 7133a4f

Browse files
authored
SONARKT-401 K2 should not create non-daemon threads
1 parent 1f24e4a commit 7133a4f

File tree

4 files changed

+137
-1
lines changed

4 files changed

+137
-1
lines changed

sonar-kotlin-api/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,9 @@ task<JavaExec>("printAst") {
5555
classpath = sourceSets.main.get().runtimeClasspath
5656
mainClass.set("org.sonarsource.kotlin.ast.AstPrinterKt")
5757
}
58+
59+
task<JavaExec>("patchKotlinCompiler") {
60+
outputs.dir(layout.buildDirectory.dir("patch"))
61+
classpath = sourceSets.main.get().runtimeClasspath
62+
mainClass.set("org.sonarsource.kotlin.tools.PatchKotlinCompilerKt")
63+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* SonarSource Kotlin
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.kotlin.tools
18+
19+
import org.jetbrains.org.objectweb.asm.ClassReader
20+
import org.jetbrains.org.objectweb.asm.ClassVisitor
21+
import org.jetbrains.org.objectweb.asm.ClassWriter
22+
import org.jetbrains.org.objectweb.asm.MethodVisitor
23+
import org.jetbrains.org.objectweb.asm.Opcodes
24+
import java.io.File
25+
26+
/**
27+
* Patches [com.intellij.util.concurrency.AppScheduledExecutorService.MyThreadFactory].
28+
*/
29+
fun main() {
30+
31+
var patched = false
32+
33+
class MyMethodVisitor(mv: MethodVisitor) : MethodVisitor(Opcodes.API_VERSION, mv) {
34+
override fun visitMethodInsn(
35+
opcode: Int,
36+
owner: String?,
37+
name: String?,
38+
descriptor: String?,
39+
isInterface: Boolean
40+
) {
41+
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
42+
if (name == "newThread") { // INVOKEINTERFACE java/util/concurrent/ThreadFactory.newThread (Ljava/lang/Runnable;)Ljava/lang/Thread; (itf)
43+
mv.visitInsn(Opcodes.DUP)
44+
mv.visitInsn(Opcodes.ICONST_1)
45+
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Thread", "setDaemon", "(Z)V", false)
46+
patched = true
47+
}
48+
}
49+
}
50+
51+
class MyClassVisitor(cv: ClassVisitor) : ClassVisitor(Opcodes.ASM9, cv) {
52+
override fun visitMethod(
53+
access: Int,
54+
name: String?,
55+
descriptor: String?,
56+
signature: String?,
57+
exceptions: Array<out String>?
58+
): MethodVisitor {
59+
return MyMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions))
60+
}
61+
}
62+
63+
val cls = "com/intellij/util/concurrency/AppScheduledExecutorService\$MyThreadFactory.class"
64+
val bytes = object {}.javaClass
65+
.getResourceAsStream("/$cls")!!
66+
.use { it.readAllBytes() }
67+
val classWriter = ClassWriter(0)
68+
ClassReader(bytes).accept(MyClassVisitor(classWriter), 0)
69+
70+
if (!patched) throw AssertionError()
71+
72+
val output = File("build/patch").resolve(cls)
73+
output.parentFile.mkdirs()
74+
File("build/patch/test.txt").writeText("test")
75+
output.writeBytes(classWriter.toByteArray())
76+
}

sonar-kotlin-plugin/build.gradle.kts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ tasks.jar {
9696
val sourcesJar = tasks.sourcesJar
9797
val javadocJar = tasks.javadocJar
9898

99+
val patchTask = project(":sonar-kotlin-api").tasks.named("patchKotlinCompiler")
100+
101+
sourceSets.main {
102+
resources.srcDir(patchTask)
103+
}
104+
99105
// Note that this task is time-consuming
100106
// and needed only for integration tests and publishing,
101107
// so it is not part of `gradle build`.
@@ -104,9 +110,17 @@ task<proguard.gradle.ProGuardTask>("dist") {
104110
description = "Assembles sonar-kotlin-plugin.jar for integration tests and publishing"
105111
libraryjars("${System.getProperty("java.home")}/jmods/java.base.jmod")
106112
injars(
107-
mapOf("filter" to "!META-INF/native/**/*jansi*,!org/jline/**,!net/jpountz/**"),
113+
mapOf(
114+
"filter" to listOf(
115+
"!com/intellij/util/concurrency/AppScheduledExecutorService\$MyThreadFactory.class", // patched version is included below
116+
"!META-INF/native/**/*jansi*",
117+
"!org/jline/**",
118+
"!net/jpountz/**"
119+
).joinToString(",")
120+
),
108121
tasks.shadowJar.get().archiveFile
109122
)
123+
injars(patchTask)
110124
outjars("build/libs/sonar-kotlin-plugin.jar")
111125
configuration("proguard.txt")
112126
doLast {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* SonarSource Kotlin
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.kotlin.plugin
18+
19+
import com.intellij.util.concurrency.AppScheduledExecutorService
20+
import org.junit.jupiter.api.Assertions.assertTrue
21+
import org.junit.jupiter.api.Test
22+
import java.util.concurrent.ScheduledExecutorService
23+
24+
private class AppScheduledExecutorServiceTest {
25+
/**
26+
* Non-daemon thread created by
27+
* [KaFirSessionProvider](https://github.com/JetBrains/kotlin/blob/v2.1.10/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/KaFirSessionProvider.kt#L67)
28+
* might prevent JVM termination - for example in case of [AssertionError]
29+
* [`ScannerMain` does not execute `System.exit`](https://github.com/SonarSource/sonar-enterprise/blob/sqs-10.8.1.101195/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/ScannerMain.java#L51-L74).
30+
*/
31+
@Test
32+
fun `should use daemon threads`() {
33+
val getInstanceMethod = AppScheduledExecutorService::class.java.getDeclaredMethod("getInstance")
34+
getInstanceMethod.isAccessible = true
35+
val instance: ScheduledExecutorService = getInstanceMethod.invoke(null) as ScheduledExecutorService
36+
instance.submit {
37+
assertTrue(Thread.currentThread().isDaemon)
38+
}.get()
39+
}
40+
}

0 commit comments

Comments
 (0)