Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ class PluginManager private constructor(
loadPlugin(pluginFile)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
} catch (e: Throwable) {
Result.failure(e)
}
result.onFailure { error -> recordLoadFailure(pluginFile, error) }
Expand Down Expand Up @@ -652,9 +652,7 @@ class PluginManager private constructor(

logger.debug("Initializing plugin: ${manifest.id}")
val initResult =
try {
plugin.initialize(pluginContext)
} catch (e: Exception) {
initializePlugin(plugin, pluginContext).getOrElse { e ->
logger.error("Plugin initialization threw exception for: ${manifest.id}", e)
if (manifest.sidebarItems > 0) {
SidebarSlotManager.releasePluginSlots(manifest.id)
Expand Down Expand Up @@ -697,7 +695,7 @@ class PluginManager private constructor(

activateLoadedPlugin(loadedPlugin)
Result.success(plugin)
} catch (e: Exception) {
} catch (e: Throwable) {
reservedSlotsPluginId?.let { pluginId ->
SidebarSlotManager.releasePluginSlots(pluginId)
}
Expand Down Expand Up @@ -783,7 +781,7 @@ class PluginManager private constructor(
} else {
logger.warn("Failed to remove documentation for plugin: $pluginId")
}
} catch (e: Exception) {
} catch (e: Throwable) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
logger.error("Error removing documentation for plugin: $pluginId", e)
}

Expand Down Expand Up @@ -1994,3 +1992,13 @@ fun LoadedPlugin.toPluginMetadata(): PluginMetadata =
iconDayPath = iconDayPath,
iconNightPath = iconNightPath,
)

/**
* Runs [IPlugin.initialize], catching Errors too: a plugin built against a newer plugin API throws
* [LinkageError] (e.g. NoClassDefFoundError) on an older CoGo, and that must disable the plugin, not
* crash the app.
*/
internal fun initializePlugin(
plugin: IPlugin,
context: PluginContext,
): Result<Boolean> = runCatching { plugin.initialize(context) }
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class PluginDocumentationManager(
withContext(Dispatchers.IO) {
try {
plugin?.onDocumentationUninstall()
} catch (e: Exception) {
} catch (e: Throwable) {
if (e is CancellationException) throw e
Log.e(TAG, "Plugin onDocumentationUninstall() threw during removal: $pluginId", e)
}
Expand Down Expand Up @@ -675,7 +675,7 @@ class PluginDocumentationManager(
recreatedCount++
}
}
} catch (e: Exception) {
} catch (e: Throwable) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
if (e is CancellationException) throw e
Log.e(TAG, "Failed to verify/recreate documentation for $pluginId", e)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.itsaky.androidide.plugins.manager.core

import com.itsaky.androidide.plugins.PluginContext
import com.itsaky.androidide.plugins.extensions.DocumentationExtension
import com.itsaky.androidide.plugins.extensions.PluginTooltipEntry
import com.itsaky.androidide.plugins.manager.documentation.PluginDocumentationManager
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import java.lang.reflect.Proxy

/**
* A plugin built against a newer plugin API, loaded by an older CoGo, throws NoClassDefFoundError
* when it first touches a missing class (GlitchTip CODEONTHEGO-10B, ADFA-6672). It must fail that
* plugin, not crash the app.
*/
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, sdk = [28])
class PluginLinkageErrorTest {
@Test
fun initializeThrowingNoClassDefFoundErrorFailsThePlugin() {
val result = initializePlugin(NewerApiPlugin(), stubContext())

assertTrue(result.exceptionOrNull() is NoClassDefFoundError)
}

@Test
fun documentationUninstallThrowingNoClassDefFoundErrorDoesNotEscape() {
val manager = PluginDocumentationManager(RuntimeEnvironment.getApplication())

val removed = runBlocking { manager.removePluginDocumentation("newer-api", NewerApiPlugin()) }

assertFalse(removed)
}

private fun stubContext(): PluginContext =
Proxy.newProxyInstance(javaClass.classLoader, arrayOf(PluginContext::class.java)) { _, _, _ ->
null
} as PluginContext

private class NewerApiPlugin : DocumentationExtension {
private fun missingClass(): Nothing =
throw NoClassDefFoundError("Failed resolution of: Lcom/itsaky/androidide/plugins/ai/LlmBackendRegistration;")

override fun initialize(context: PluginContext): Boolean = missingClass()

override fun onDocumentationUninstall() = missingClass()

override fun activate() = true

override fun deactivate() = true

override fun dispose() {}

override fun getTooltipCategory() = "newer-api"

override fun getTooltipEntries() = emptyList<PluginTooltipEntry>()
}
}
Loading