diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/AbstractCompilationEnvironment.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/AbstractCompilationEnvironment.kt index cc5550c987..178ebe3d29 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/AbstractCompilationEnvironment.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/AbstractCompilationEnvironment.kt @@ -4,12 +4,12 @@ import com.itsaky.androidide.lsp.kotlin.compiler.index.KtSymbolIndex import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule import com.itsaky.androidide.lsp.kotlin.compiler.modules.asFlatSequence import com.itsaky.androidide.lsp.kotlin.compiler.modules.isSourceModule -import com.itsaky.androidide.lsp.kotlin.compiler.registrar.AnalysisApiServiceProvider import com.itsaky.androidide.lsp.kotlin.compiler.services.JavaModuleAccessibilityChecker import com.itsaky.androidide.lsp.kotlin.compiler.services.JavaModuleAnnotationsProvider import com.itsaky.androidide.lsp.kotlin.compiler.services.KtLspService import com.itsaky.androidide.lsp.kotlin.compiler.services.WriteAccessGuard import com.itsaky.androidide.lsp.kotlin.compiler.services.latestLanguageVersionSettings +import com.itsaky.androidide.lsp.kotlin.compiler.util.SLF4JLogger import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull import org.jetbrains.kotlin.K1Deprecation @@ -53,6 +53,7 @@ import org.jetbrains.kotlin.com.intellij.core.CorePackageIndex import org.jetbrains.kotlin.com.intellij.ide.highlighter.JavaFileType import org.jetbrains.kotlin.com.intellij.mock.MockApplication import org.jetbrains.kotlin.com.intellij.mock.MockProject +import org.jetbrains.kotlin.com.intellij.openapi.diagnostic.Logger import org.jetbrains.kotlin.com.intellij.openapi.editor.impl.DocumentWriteAccessGuard import org.jetbrains.kotlin.com.intellij.openapi.roots.PackageIndex import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer @@ -79,6 +80,7 @@ import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil import org.jetbrains.kotlin.psi.KtPsiFactory import java.nio.file.Path import kotlin.io.path.pathString +import kotlin.time.Duration.Companion.seconds /** * Base class shared by [CompilationEnvironment] (production) and the test-only @@ -99,11 +101,13 @@ internal abstract class AbstractCompilationEnvironment( companion object { /** Max time close() will block the (main) thread draining background workers before disposal. */ - const val CLOSE_DRAIN_TIMEOUT_MS = 2_000L + val CLOSE_DRAIN_TIMEOUT = 2.seconds init { System.setProperty("java.awt.headless", "true") setupIdeaStandaloneExecution() + + Logger.setFactory { name -> SLF4JLogger(name) } } } @@ -345,7 +349,7 @@ internal abstract class AbstractCompilationEnvironment( // teardown, so the join is bounded by a timeout to avoid an ANR if a read is slow; the // project.isDisposed guards cover the rare case where the timeout fires before draining. if (::ktSymbolIndex.isInitialized) { - runBlocking { withTimeoutOrNull(CLOSE_DRAIN_TIMEOUT_MS) { ktSymbolIndex.close() } } + runBlocking { withTimeoutOrNull(CLOSE_DRAIN_TIMEOUT) { ktSymbolIndex.close() } } } Disposer.dispose(disposable) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt index 0702aa0694..1db2911f30 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt @@ -307,7 +307,7 @@ internal class CompilationEnvironment( // super.close() disposes the project, so an in-flight read can't touch a disposed project // (APPDEVFORALL-17R / ADFA-4384). Bounded so a slow read can't block shutdown indefinitely. runBlocking { - withTimeoutOrNull(CLOSE_DRAIN_TIMEOUT_MS) { + withTimeoutOrNull(CLOSE_DRAIN_TIMEOUT) { coroutineScope.coroutineContext[Job]?.cancelAndJoin() } } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLogger.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLogger.kt new file mode 100644 index 0000000000..dac66e7160 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLogger.kt @@ -0,0 +1,57 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.util + +import org.jetbrains.kotlin.com.intellij.openapi.diagnostic.Logger +import org.slf4j.LoggerFactory + +class SLF4JLogger(name: String) : Logger() { + private val logger = LoggerFactory.getLogger(name) + + override fun isDebugEnabled(): Boolean { + return logger.isDebugEnabled + } + + override fun debug(message: String?, t: Throwable?) { + if (t != null) { + logger.debug(message ?: "", t) + } else { + logger.debug(message ?: "") + } + } + + override fun info(message: String?, t: Throwable?) { + if (t != null) { + logger.info(message ?: "", t) + } else { + logger.info(message ?: "") + } + } + + override fun warn(message: String?, t: Throwable?) { + if (t != null) { + logger.warn(message ?: "", t) + } else { + logger.warn(message ?: "") + } + } + + override fun error(message: String?, t: Throwable?, vararg details: String?) { + val msg = message ?: "" + val detailStr = if (details.isNotEmpty()) { + details.filterNotNull().joinToString(System.lineSeparator()) + } else { + "" + } + + val fullMessage = if (detailStr.isNotEmpty()) { + if (msg.isNotEmpty()) "$msg: $detailStr" else detailStr + } else { + msg + } + + if (t != null) { + logger.error(fullMessage, t) + } else { + logger.error(fullMessage) + } + } +} diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLoggerTest.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLoggerTest.kt new file mode 100644 index 0000000000..bd89089b76 --- /dev/null +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLoggerTest.kt @@ -0,0 +1,24 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.util + +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.lsp.kotlin.fixtures.KtLspTest +import org.jetbrains.kotlin.com.intellij.openapi.diagnostic.Logger +import org.junit.Test + +class SLF4JLoggerTest : KtLspTest() { + + @Test + fun `compiler logger factory routes to SLF4JLogger`() { + assertThat(Logger.getInstance("ADFA-4238")).isInstanceOf(SLF4JLogger::class.java) + } + + @Test + fun `error does not throw - regression guard for ADFA-4238`() { + // DefaultLogger.error() rethrows as AssertionError, escalating a recoverable FIR cache + // inconsistency into a crash (Sentry APPDEVFORALL-13D). SLF4JLogger must log, not throw. + val logger = Logger.getInstance("ADFA-4238") + logger.error("Inconsistency in the cache. Someone without context put a null value in the cache") + logger.error("with throwable", RuntimeException("boom")) + logger.error("with details", RuntimeException(), "detail-1", "detail-2") + } +}