From 78a167aacf29e86d959c3b7679714c6b255a3931 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 30 Jul 2023 16:29:40 +0200 Subject: [PATCH] Fix/webui setup failure in case bundled webui is missing (#625) * Rename functions * Require version to be passed to "downloadVersion" Makes it possible to download different versions than the latest compatible one with retry functionality * Fallback to downloading bundled webUI in case it's missing In case no download was possible and the fallback to the bundled version also failed due to it not existing, try to download the version of the bundled version as a last resort. * Handle exception of "getLatestCompatibleVersion" * Move validation of download to actual download function * Extract retry logic into function * Retry every fetch up to 3 times * Log full exception and change log level --- .../server/util/WebInterfaceManager.kt | 114 +++++++++++------- 1 file changed, 70 insertions(+), 44 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt index ca86af92..fcc42e3b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt @@ -11,6 +11,7 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonPrimitive +import mu.KLogger import mu.KotlinLogging import net.lingala.zip4j.ZipFile import org.json.JSONArray @@ -36,6 +37,8 @@ private val tmpDir = System.getProperty("java.io.tmpdir") private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } +class BundledWebUIMissing : Exception("No bundled webUI version found") + enum class WebUIChannel { BUNDLED, // the default webUI version bundled with the server release STABLE, @@ -147,16 +150,17 @@ object WebInterfaceManager { * * In case the download failed but the local webUI is valid the download is considered a success to prevent the fallback logic */ - val doDownload = { + val doDownload: (getVersion: () -> String) -> Boolean = { getVersion -> try { - downloadLatestCompatibleVersion() + downloadVersion(getVersion()) + true } catch (e: Exception) { false } || isLocalWebUIValid } // download the latest compatible version for the current selected webUI - val fallbackToDefaultWebUI = !doDownload() + val fallbackToDefaultWebUI = !doDownload() { getLatestCompatibleVersion() } if (!fallbackToDefaultWebUI) { return } @@ -166,7 +170,7 @@ object WebInterfaceManager { serverConfig.webUIFlavor = DEFAULT_WEB_UI - val fallbackToBundledVersion = !doDownload() + val fallbackToBundledVersion = !doDownload() { getLatestCompatibleVersion() } if (!fallbackToBundledVersion) { return } @@ -174,11 +178,21 @@ object WebInterfaceManager { logger.warn { "doInitialSetup: fallback to bundled default webUI \"$DEFAULT_WEB_UI\"" } - extractBundledWebUI() + try { + extractBundledWebUI() + return + } catch (e: BundledWebUIMissing) { + logger.warn(e) { "doInitialSetup: fallback to downloading the version of the bundled webUI" } + } + + val downloadFailed = !doDownload() { BuildConfig.WEBUI_TAG } + if (downloadFailed) { + throw Exception("Unable to setup a webUI") + } } private fun extractBundledWebUI() { - val resourceWebUI: InputStream = BuildConfig::class.java.getResourceAsStream("/WebUI.zip") ?: throw Error("extractBundledWebUI: No bundled webUI version found") + val resourceWebUI: InputStream = BuildConfig::class.java.getResourceAsStream("/WebUI.zip") ?: throw BundledWebUIMissing() logger.info { "extractBundledWebUI: Using the bundled WebUI zip..." } @@ -205,7 +219,11 @@ object WebInterfaceManager { } logger.info { "checkForUpdate(${serverConfig.webUIFlavor}, $localVersion): An update is available, starting download..." } - downloadLatestCompatibleVersion() + try { + downloadVersion(getLatestCompatibleVersion()) + } catch (e: Exception) { + logger.warn(e) { "checkForUpdate: failed due to" } + } } private fun getDownloadUrlFor(version: String): String { @@ -259,10 +277,26 @@ object WebInterfaceManager { return digest.toHex() } + private fun executeWithRetry(log: KLogger, execute: () -> T, maxRetries: Int = 3, retryCount: Int = 0): T { + try { + return execute() + } catch (e: Exception) { + log.warn(e) { "(retry $retryCount/$maxRetries) failed due to" } + + if (retryCount < maxRetries) { + return executeWithRetry(log, execute, maxRetries, retryCount + 1) + } + + throw e + } + } + private fun fetchMD5SumFor(version: String): String { return try { - val url = "${getDownloadUrlFor(version)}/md5sum" - URL(url).readText().trim() + executeWithRetry(KotlinLogging.logger("${logger.name} fetchMD5SumFor($version)"), { + val url = "${getDownloadUrlFor(version)}/md5sum" + URL(url).readText().trim() + }) } catch (e: Exception) { "" } @@ -274,8 +308,14 @@ object WebInterfaceManager { } private fun fetchPreviewVersion(): String { - val releaseInfoJson = URL(WebUI.WEBUI.latestReleaseInfoUrl).readText() - return Json.decodeFromString(releaseInfoJson)["tag_name"]?.jsonPrimitive?.content ?: throw Exception("Failed to get the preview version tag") + return executeWithRetry(KotlinLogging.logger("${logger.name} fetchPreviewVersion"), { + val releaseInfoJson = URL(WebUI.WEBUI.latestReleaseInfoUrl).readText() + Json.decodeFromString(releaseInfoJson)["tag_name"]?.jsonPrimitive?.content ?: throw Exception("Failed to get the preview version tag") + }) + } + + private fun fetchServerMappingFile(): JSONArray { + return executeWithRetry(KotlinLogging.logger("$logger fetchServerMappingFile"), { JSONArray(URL(WebUI.WEBUI.versionMappingUrl).readText()) }) } private fun getLatestCompatibleVersion(): String { @@ -285,7 +325,7 @@ object WebInterfaceManager { } val currentServerVersionNumber = extractVersion(BuildConfig.REVISION) - val webUIToServerVersionMappings = JSONArray(URL(WebUI.WEBUI.versionMappingUrl).readText()) + val webUIToServerVersionMappings = fetchServerMappingFile() logger.debug { "getLatestCompatibleVersion: webUIChannel= ${serverConfig.webUIChannel}, currentServerVersion= ${BuildConfig.REVISION}, mappingFile= $webUIToServerVersionMappings" } @@ -311,45 +351,27 @@ object WebInterfaceManager { throw Exception("No compatible webUI version found") } - fun downloadLatestCompatibleVersion(retryCount: Int = 0): Boolean { - val latestCompatibleVersion = getLatestCompatibleVersion() - - val webUIZip = "${WebUI.WEBUI.baseFileName}-$latestCompatibleVersion.zip" + fun downloadVersion(version: String) { + val webUIZip = "${WebUI.WEBUI.baseFileName}-$version.zip" val webUIZipPath = "$tmpDir/$webUIZip" - val webUIZipFile = File(webUIZipPath) + val webUIZipURL = "${getDownloadUrlFor(version)}/$webUIZip" - logger.info { "downloadLatestCompatibleVersion: Downloading WebUI (flavor= ${serverConfig.webUIFlavor}, version \"$latestCompatibleVersion\") zip from the Internet..." } - - try { - val webUIZipURL = "${getDownloadUrlFor(latestCompatibleVersion)}/$webUIZip" - downloadVersion(webUIZipURL, webUIZipFile) - - if (!isDownloadValid(webUIZip, webUIZipPath)) { - throw Exception("Download is invalid") - } - } catch (e: Exception) { - val retry = retryCount < 3 - logger.error { "downloadLatestCompatibleVersion: Download failed${if (retry) ", retrying ${retryCount + 1}/3" else ""} - error: $e" } - - if (retry) { - return downloadLatestCompatibleVersion(retryCount + 1) - } - - return false - } + val log = KotlinLogging.logger("${logger.name} downloadVersion(version= $version, flavor= ${serverConfig.webUIFlavor})") + log.info { "Downloading WebUI zip from the Internet..." } + executeWithRetry(log, { downloadVersionZipFile(webUIZipURL, webUIZipPath) }) File(applicationDirs.webUIRoot).deleteRecursively() // extract webUI zip - logger.info { "downloadLatestCompatibleVersion: Extracting WebUI zip..." } + log.info { "Extracting WebUI zip..." } extractDownload(webUIZipPath, applicationDirs.webUIRoot) - logger.info { "downloadLatestCompatibleVersion: Extracting WebUI zip Done." } - - return true + log.info { "Extracting WebUI zip Done." } } - private fun downloadVersion(url: String, zipFile: File) { + private fun downloadVersionZipFile(url: String, filePath: String) { + val zipFile = File(filePath) zipFile.delete() + val data = ByteArray(1024) zipFile.outputStream().use { webUIZipFileOut -> @@ -361,7 +383,7 @@ object WebInterfaceManager { connection.inputStream.buffered().use { inp -> var totalCount = 0 - print("downloadVersion: Download progress: % 00") + print("downloadVersionZipFile: Download progress: % 00") while (true) { val count = inp.read(data, 0, 1024) @@ -377,9 +399,13 @@ object WebInterfaceManager { webUIZipFileOut.write(data, 0, count) } println() - logger.info { "downloadVersion: Downloading WebUI Done." } + logger.info { "downloadVersionZipFile: Downloading WebUI Done." } } } + + if (!isDownloadValid(zipFile.name, filePath)) { + throw Exception("Download is invalid") + } } private fun isDownloadValid(zipFileName: String, zipFilePath: String): Boolean { @@ -404,7 +430,7 @@ object WebInterfaceManager { val latestCompatibleVersion = getLatestCompatibleVersion() latestCompatibleVersion != currentVersion } catch (e: Exception) { - logger.debug { "isUpdateAvailable: check failed due to $e" } + logger.warn(e) { "isUpdateAvailable: check failed due to" } false } }