Fix: KotlinNativeABICompatibilityChecker.dispose() leads to dead lock

Issue #KT-36040
This commit is contained in:
Dmitriy Dolovov
2020-01-23 19:49:11 +07:00
parent d1e14598a6
commit 345863a1d2
2 changed files with 268 additions and 30 deletions
@@ -9,20 +9,17 @@ import com.intellij.ProjectTopics
import com.intellij.notification.Notification import com.intellij.notification.Notification
import com.intellij.notification.NotificationType import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.*
import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.ReadAction.nonBlocking
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.components.ProjectComponent import com.intellij.openapi.components.ProjectComponent
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.roots.ModuleRootEvent import com.intellij.openapi.roots.ModuleRootEvent
import com.intellij.openapi.roots.ModuleRootListener import com.intellij.openapi.roots.ModuleRootListener
import com.intellij.openapi.util.text.StringUtilRt import com.intellij.openapi.util.text.StringUtilRt
import com.intellij.util.PathUtilRt import com.intellij.util.PathUtilRt
import com.intellij.util.concurrency.AppExecutorUtil
import org.jetbrains.concurrency.CancellablePromise
import org.jetbrains.kotlin.idea.caches.project.getModuleInfosFromIdeaModel import org.jetbrains.kotlin.idea.caches.project.getModuleInfosFromIdeaModel
import org.jetbrains.kotlin.idea.configuration.KotlinNativeLibraryNameUtil import org.jetbrains.kotlin.idea.configuration.KotlinNativeLibraryNameUtil
import org.jetbrains.kotlin.idea.versions.UnsupportedAbiVersionNotificationPanelProvider import org.jetbrains.kotlin.idea.versions.UnsupportedAbiVersionNotificationPanelProvider
@@ -47,6 +44,9 @@ class KotlinNativeABICompatibilityChecker(private val project: Project) : Projec
private val cachedIncompatibleLibraries = HashSet<String>() private val cachedIncompatibleLibraries = HashSet<String>()
@Volatile
private var backgroundJob: CancellablePromise<*>? = null
init { init {
project.messageBus.connect().subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener { project.messageBus.connect().subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener {
override fun rootsChanged(event: ModuleRootEvent) { override fun rootsChanged(event: ModuleRootEvent) {
@@ -65,29 +65,25 @@ class KotlinNativeABICompatibilityChecker(private val project: Project) : Projec
if (ApplicationManager.getApplication().isUnitTestMode || project.isDisposed) if (ApplicationManager.getApplication().isUnitTestMode || project.isDisposed)
return return
ProgressManager.getInstance().runProcessWithProgressAsynchronously( backgroundJob = nonBlocking<List<Notification>> {
object : Task.Backgroundable(project, BG_TASK_NAME) { val librariesToNotify = getLibrariesToNotifyAbout()
override fun run(indicator: ProgressIndicator) { prepareNotifications(librariesToNotify)
val librariesToNotify = runReadAction { }
if (project.isDisposed) return@runReadAction emptyMap<String, NativeLibraryInfo>() .finishOnUiThread(ModalityState.defaultModalityState()) { notifications ->
notifications.forEach {
getLibrariesToNotifyAbout() it.notify(project)
}
if (project.isDisposed) return
val notifications = prepareNotifications(librariesToNotify)
notifications.forEach {
runInEdt {
it.notify(project)
}
}
} }
}, }
EmptyProgressIndicator() .expireWith(project) // cancel job when project is disposed
) .coalesceBy(this@KotlinNativeABICompatibilityChecker) // cancel previous job when new one is submitted
.withDocumentsCommitted(project)
.submit(AppExecutorUtil.getAppExecutorService())
.onProcessed {
backgroundJob = null
}
} }
private fun getLibrariesToNotifyAbout(): Map<String, NativeLibraryInfo> = synchronized(this) { private fun getLibrariesToNotifyAbout(): Map<String, NativeLibraryInfo> {
val incompatibleLibraries = getModuleInfosFromIdeaModel(project).asSequence() val incompatibleLibraries = getModuleInfosFromIdeaModel(project).asSequence()
.filterIsInstance<NativeLibraryInfo>() .filterIsInstance<NativeLibraryInfo>()
.filter { !it.metadataInfo.isCompatible } .filter { !it.metadataInfo.isCompatible }
@@ -219,7 +215,10 @@ class KotlinNativeABICompatibilityChecker(private val project: Project) : Projec
return (ideLibraryName ?: PathUtilRt.getFileName(libraryInfo.libraryRoot)) to LibraryGroup.User return (ideLibraryName ?: PathUtilRt.getFileName(libraryInfo.libraryRoot)) to LibraryGroup.User
} }
override fun dispose() = synchronized(this) { override fun dispose() {
backgroundJob?.let(CancellablePromise<*>::cancel)
backgroundJob = null
cachedIncompatibleLibraries.clear() cachedIncompatibleLibraries.clear()
} }
@@ -237,7 +236,5 @@ class KotlinNativeABICompatibilityChecker(private val project: Project) : Projec
private const val NOTIFICATION_TITLE = "Incompatible Kotlin/Native libraries" private const val NOTIFICATION_TITLE = "Incompatible Kotlin/Native libraries"
private const val NOTIFICATION_GROUP_ID = NOTIFICATION_TITLE private const val NOTIFICATION_GROUP_ID = NOTIFICATION_TITLE
private const val BG_TASK_NAME = "Finding incompatible Kotlin/Native libraries"
} }
} }
@@ -0,0 +1,241 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.ide.konan
import com.intellij.ProjectTopics
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.*
import com.intellij.openapi.application.ReadAction.nonBlocking
import com.intellij.openapi.components.ProjectComponent
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.roots.ModuleRootEvent
import com.intellij.openapi.roots.ModuleRootListener
import com.intellij.openapi.util.text.StringUtilRt
import com.intellij.util.PathUtilRt
import com.intellij.util.concurrency.AppExecutorUtil
import org.jetbrains.concurrency.CancellablePromise
import org.jetbrains.kotlin.idea.caches.project.getModuleInfosFromIdeaModel
import org.jetbrains.kotlin.idea.configuration.KotlinNativeLibraryNameUtil
import org.jetbrains.kotlin.idea.versions.UnsupportedAbiVersionNotificationPanelProvider
import org.jetbrains.kotlin.idea.versions.bundledRuntimeVersion
import org.jetbrains.kotlin.konan.library.KONAN_STDLIB_NAME
/** TODO: merge [KotlinNativeABICompatibilityChecker] in the future with [UnsupportedAbiVersionNotificationPanelProvider], KT-34525 */
class KotlinNativeABICompatibilityChecker(private val project: Project) : ProjectComponent, Disposable {
private sealed class LibraryGroup(private val ordinal: Int) : Comparable<LibraryGroup> {
override fun compareTo(other: LibraryGroup) = when {
this == other -> 0
this is FromDistribution && other is FromDistribution -> kotlinVersion.compareTo(other.kotlinVersion)
else -> ordinal.compareTo(other.ordinal)
}
data class FromDistribution(val kotlinVersion: String) : LibraryGroup(0)
object ThirdParty : LibraryGroup(1)
object User : LibraryGroup(2)
}
private val cachedIncompatibleLibraries = HashSet<String>()
@Volatile
private var backgroundJob: CancellablePromise<*>? = null
init {
project.messageBus.connect().subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener {
override fun rootsChanged(event: ModuleRootEvent) {
// run when project roots are changes, e.g. on project import
validateKotlinNativeLibraries()
}
})
}
override fun projectOpened() {
// run when project is opened
validateKotlinNativeLibraries()
}
private fun validateKotlinNativeLibraries() {
if (ApplicationManager.getApplication().isUnitTestMode || project.isDisposed)
return
backgroundJob = nonBlocking<List<Notification>> {
val librariesToNotify = getLibrariesToNotifyAbout()
prepareNotifications(librariesToNotify)
}
.finishOnUiThread(ModalityState.defaultModalityState()) { notifications ->
notifications.forEach {
it.notify(project)
}
}
.expireWith(project) // cancel job when project is disposed
.withDocumentsCommitted(project)
.submit(AppExecutorUtil.getAppExecutorService())
.also {
it.onProcessed {
backgroundJob = null
}
}
}
private fun getLibrariesToNotifyAbout(): Map<String, NativeLibraryInfo> {
val incompatibleLibraries = getModuleInfosFromIdeaModel(project).asSequence()
.filterIsInstance<NativeLibraryInfo>()
.filter { !it.metadataInfo.isCompatible }
.associateBy { it.libraryRoot }
val newEntries = if (cachedIncompatibleLibraries.isNotEmpty())
incompatibleLibraries.filterKeys { it !in cachedIncompatibleLibraries }
else
incompatibleLibraries
cachedIncompatibleLibraries.clear()
cachedIncompatibleLibraries.addAll(incompatibleLibraries.keys)
return newEntries
}
private fun prepareNotifications(librariesToNotify: Map<String, NativeLibraryInfo>): List<Notification> {
if (librariesToNotify.isEmpty())
return emptyList()
val librariesByGroups = HashMap<Pair<LibraryGroup, Boolean>, MutableList<Pair<String, String>>>()
librariesToNotify.forEach { (libraryRoot, libraryInfo) ->
val isOldMetadata = (libraryInfo.metadataInfo as? NativeLibraryInfo.MetadataInfo.Incompatible)?.isOlder ?: true
val (libraryName, libraryGroup) = parseIDELibraryName(libraryInfo)
librariesByGroups.computeIfAbsent(libraryGroup to isOldMetadata) { mutableListOf() } += libraryName to libraryRoot
}
return librariesByGroups.keys.sortedWith(
compareBy(
{ (libraryGroup, _) -> libraryGroup },
{ (_, isOldMetadata) -> isOldMetadata }
)
).map { key ->
val (libraryGroup, isOldMetadata) = key
val libraries =
librariesByGroups.getValue(key).sortedWith(compareBy(LIBRARY_NAME_COMPARATOR) { (libraryName, _) -> libraryName })
val compilerVersionText = if (isOldMetadata) "an older" else "a newer"
val message = when (libraryGroup) {
is LibraryGroup.FromDistribution -> {
val libraryNamesInOneLine =
libraries.joinToString(limit = MAX_LIBRARY_NAMES_IN_ONE_LINE) { (libraryName, _) -> libraryName }
"""
|There are ${libraries.size} libraries from the Kotlin/Native ${libraryGroup.kotlinVersion} distribution attached to the project: $libraryNamesInOneLine
|
|These libraries were compiled with $compilerVersionText Kotlin/Native compiler and can't be read in IDE. Please edit Gradle buildfile(s) to use Kotlin Gradle plugin version ${bundledRuntimeVersion()}. Then re-import the project in IDE.
""".trimMargin()
}
is LibraryGroup.ThirdParty -> {
if (libraries.size == 1) {
"""
|There is a third-party library attached to the project that was compiled with $compilerVersionText Kotlin/Native compiler and can't be read in IDE: ${libraries.single()
.first}
|
|Please edit Gradle buildfile(s) and specify library version compatible with Kotlin/Native ${bundledRuntimeVersion()}. Then re-import the project in IDE.
""".trimMargin()
} else {
val librariesLineByLine = libraries.joinToString(separator = "\n") { (libraryName, _) -> libraryName }
"""
|There are ${libraries.size} third-party libraries attached to the project that were compiled with $compilerVersionText Kotlin/Native compiler and can't be read in IDE:
|$librariesLineByLine
|
|Please edit Gradle buildfile(s) and specify library versions compatible with Kotlin/Native ${bundledRuntimeVersion()}. Then re-import the project in IDE.
""".trimMargin()
}
}
is LibraryGroup.User -> {
val projectRoot = project.guessProjectDir()?.canonicalPath
fun getLibraryTextToPrint(libraryNameAndRoot: Pair<String, String>): String {
val (libraryName, libraryRoot) = libraryNameAndRoot
val relativeRoot = projectRoot?.let {
libraryRoot.substringAfter(projectRoot)
.takeIf { it != libraryRoot }
?.trimStart('/', '\\')
?.let { "${'$'}project/$it" }
} ?: libraryRoot
return "\"$libraryName\" at $relativeRoot"
}
if (libraries.size == 1) {
"""
|There is a library attached to the project that was compiled with $compilerVersionText Kotlin/Native compiler and can't be read in IDE:
|${getLibraryTextToPrint(libraries.single())}
|
|Please edit Gradle buildfile(s) to use Kotlin Gradle plugin version ${bundledRuntimeVersion()}. Then rebuild the project and re-import it in IDE.
""".trimMargin()
} else {
val librariesLineByLine = libraries.joinToString(separator = "\n", transform = ::getLibraryTextToPrint)
"""
|There are ${libraries.size} libraries attached to the project that were compiled with $compilerVersionText Kotlin/Native compiler and can't be read in IDE:
|$librariesLineByLine
|
|Please edit Gradle buildfile(s) to use Kotlin Gradle plugin version ${bundledRuntimeVersion()}. Then rebuild the project and re-import it in IDE.
""".trimMargin()
}
}
}
Notification(
NOTIFICATION_GROUP_ID,
NOTIFICATION_TITLE,
StringUtilRt.convertLineSeparators(message, "<br/>"),
NotificationType.ERROR,
null
)
}
}
// returns pair of library name and library group
private fun parseIDELibraryName(libraryInfo: NativeLibraryInfo): Pair<String, LibraryGroup> {
val ideLibraryName = libraryInfo.library.name?.takeIf(String::isNotEmpty)
if (ideLibraryName != null) {
KotlinNativeLibraryNameUtil.parseIDELibraryName(ideLibraryName)?.let { (kotlinVersion, libraryName) ->
return libraryName to LibraryGroup.FromDistribution(kotlinVersion)
}
if (KotlinNativeLibraryNameUtil.isGradleLibraryName(ideLibraryName))
return ideLibraryName to LibraryGroup.ThirdParty
}
return (ideLibraryName ?: PathUtilRt.getFileName(libraryInfo.libraryRoot)) to LibraryGroup.User
}
override fun dispose() {
backgroundJob?.let(CancellablePromise<*>::cancel)
backgroundJob = null
cachedIncompatibleLibraries.clear()
}
companion object {
private val LIBRARY_NAME_COMPARATOR = Comparator<String> { libraryName1, libraryName2 ->
when {
libraryName1 == libraryName2 -> 0
libraryName1 == KONAN_STDLIB_NAME -> -1 // stdlib must go the first
libraryName2 == KONAN_STDLIB_NAME -> 1
else -> libraryName1.compareTo(libraryName2)
}
}
private const val MAX_LIBRARY_NAMES_IN_ONE_LINE = 5
private const val NOTIFICATION_TITLE = "Incompatible Kotlin/Native libraries"
private const val NOTIFICATION_GROUP_ID = NOTIFICATION_TITLE
}
}