[#1974] Uninstall extension completely on install failure (#1975)

* [#1974] Uninstall extension completely on install failure

* Add changelog entry

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
This commit is contained in:
Constantin Piber
2026-05-05 16:04:50 +02:00
committed by GitHub
parent 6bc5046773
commit 76686db6a1
2 changed files with 63 additions and 55 deletions
+1
View File
@@ -50,6 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- (**Source/API**) Fix handling of nullable preference keys (TYPE "preferences") - (**Source/API**) Fix handling of nullable preference keys (TYPE "preferences")
- (**Source**) Fix local manga thumbnails handling - (**Source**) Fix local manga thumbnails handling
- (**Extension**) Fix missing icon for manually installed source - (**Extension**) Fix missing icon for manually installed source
- (**Extension**) Fix installation retry always fails
- (**Extension**) Fixed a java.lang.VerifyError when installing an extension that has ProGuard enabled. - (**Extension**) Fixed a java.lang.VerifyError when installing an extension that has ProGuard enabled.
- (**Backup**) Fix importing of backups with missing server settings - (**Backup**) Fix importing of backups with missing server settings
- (**Backup**) Fix importing of backups with invalid server settings - (**Backup**) Fix importing of backups with invalid server settings
@@ -119,7 +119,6 @@ object Extension {
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType" val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
val jarFilePath = "$dirPathWithoutType.jar" val jarFilePath = "$dirPathWithoutType.jar"
val dexFilePath = "$dirPathWithoutType.dex"
val packageInfo = getPackageInfo(apkFilePath) val packageInfo = getPackageInfo(apkFilePath)
val pkgName = packageInfo.packageName val pkgName = packageInfo.packageName
@@ -163,71 +162,79 @@ object Extension {
// clean up // clean up
File(apkFilePath).delete() File(apkFilePath).delete()
File(dexFilePath).delete()
// collect sources from the extension try {
val extensionMainClassInstance = loadExtensionSources(jarFilePath, className) // collect sources from the extension
val sources: List<CatalogueSource> = val extensionMainClassInstance = loadExtensionSources(jarFilePath, className)
when (extensionMainClassInstance) { val sources: List<CatalogueSource> =
is Source -> listOf(extensionMainClassInstance) when (extensionMainClassInstance) {
is SourceFactory -> extensionMainClassInstance.createSources() is Source -> listOf(extensionMainClassInstance)
else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}") is SourceFactory -> extensionMainClassInstance.createSources()
}.map { it as CatalogueSource } else -> throw RuntimeException("Unknown source class type! ${extensionMainClassInstance.javaClass}")
}.map { it as CatalogueSource }
val langs = sources.map { it.lang }.toSet() val langs = sources.map { it.lang }.toSet()
val extensionLang = val extensionLang =
when (langs.size) { when (langs.size) {
0 -> "" 0 -> ""
1 -> langs.first() 1 -> langs.first()
else -> "all" else -> "all"
} }
val extensionName = val extensionName =
packageInfo.applicationInfo.nonLocalizedLabel packageInfo.applicationInfo.nonLocalizedLabel
.toString() .toString()
.substringAfter("Tachiyomi: ") .substringAfter("Tachiyomi: ")
// update extension info // update extension info
transaction { transaction {
if (ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) { if (ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.firstOrNull() == null) {
ExtensionTable.insert { ExtensionTable.insert {
it[this.apkName] = apkName
it[name] = extensionName
it[this.pkgName] = packageInfo.packageName
it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode
it[lang] = extensionLang
it[this.isNsfw] = isNsfw
}
}
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
it[this.apkName] = apkName it[this.apkName] = apkName
it[name] = extensionName it[this.isInstalled] = true
it[this.pkgName] = packageInfo.packageName it[this.classFQName] = className
it[versionName] = packageInfo.versionName it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode it[versionCode] = packageInfo.versionCode
it[lang] = extensionLang }
it[this.isNsfw] = isNsfw
val extensionId =
ExtensionTable
.selectAll()
.where { ExtensionTable.pkgName eq pkgName }
.first()[ExtensionTable.id]
.value
sources.forEach { httpSource ->
SourceTable.insert {
it[id] = httpSource.id
it[name] = httpSource.name
it[lang] = httpSource.lang
it[extension] = extensionId
it[SourceTable.isNsfw] = isNsfw
}
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
} }
} }
return 201 // we installed successfully
} catch (e: Throwable) {
// free up the file descriptor if exists
PackageTools.jarLoaderMap.remove(jarFilePath)?.close()
File(jarFilePath).delete()
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) { uninstallExtension(pkgName)
it[this.apkName] = apkName throw e
it[this.isInstalled] = true
it[this.classFQName] = className
it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode
}
val extensionId =
ExtensionTable
.selectAll()
.where { ExtensionTable.pkgName eq pkgName }
.first()[ExtensionTable.id]
.value
sources.forEach { httpSource ->
SourceTable.insert {
it[id] = httpSource.id
it[name] = httpSource.name
it[lang] = httpSource.lang
it[extension] = extensionId
it[SourceTable.isNsfw] = isNsfw
}
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
}
} }
return 201 // we installed successfully
} else { } else {
return 302 // extension was already installed return 302 // extension was already installed
} }