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
This commit is contained in:
schroda
2023-07-30 16:29:40 +02:00
committed by GitHub
parent 5a913fdfbb
commit 78a167aacf

View File

@@ -11,6 +11,7 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import mu.KLogger
import mu.KotlinLogging import mu.KotlinLogging
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import org.json.JSONArray 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) } private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
class BundledWebUIMissing : Exception("No bundled webUI version found")
enum class WebUIChannel { enum class WebUIChannel {
BUNDLED, // the default webUI version bundled with the server release BUNDLED, // the default webUI version bundled with the server release
STABLE, 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 * 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 { try {
downloadLatestCompatibleVersion() downloadVersion(getVersion())
true
} catch (e: Exception) { } catch (e: Exception) {
false false
} || isLocalWebUIValid } || isLocalWebUIValid
} }
// download the latest compatible version for the current selected webUI // download the latest compatible version for the current selected webUI
val fallbackToDefaultWebUI = !doDownload() val fallbackToDefaultWebUI = !doDownload() { getLatestCompatibleVersion() }
if (!fallbackToDefaultWebUI) { if (!fallbackToDefaultWebUI) {
return return
} }
@@ -166,7 +170,7 @@ object WebInterfaceManager {
serverConfig.webUIFlavor = DEFAULT_WEB_UI serverConfig.webUIFlavor = DEFAULT_WEB_UI
val fallbackToBundledVersion = !doDownload() val fallbackToBundledVersion = !doDownload() { getLatestCompatibleVersion() }
if (!fallbackToBundledVersion) { if (!fallbackToBundledVersion) {
return return
} }
@@ -174,11 +178,21 @@ object WebInterfaceManager {
logger.warn { "doInitialSetup: fallback to bundled default webUI \"$DEFAULT_WEB_UI\"" } 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() { 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..." } 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..." } 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 { private fun getDownloadUrlFor(version: String): String {
@@ -259,10 +277,26 @@ object WebInterfaceManager {
return digest.toHex() return digest.toHex()
} }
private fun <T> 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 { private fun fetchMD5SumFor(version: String): String {
return try { return try {
val url = "${getDownloadUrlFor(version)}/md5sum" executeWithRetry(KotlinLogging.logger("${logger.name} fetchMD5SumFor($version)"), {
URL(url).readText().trim() val url = "${getDownloadUrlFor(version)}/md5sum"
URL(url).readText().trim()
})
} catch (e: Exception) { } catch (e: Exception) {
"" ""
} }
@@ -274,8 +308,14 @@ object WebInterfaceManager {
} }
private fun fetchPreviewVersion(): String { private fun fetchPreviewVersion(): String {
val releaseInfoJson = URL(WebUI.WEBUI.latestReleaseInfoUrl).readText() return executeWithRetry(KotlinLogging.logger("${logger.name} fetchPreviewVersion"), {
return Json.decodeFromString<JsonObject>(releaseInfoJson)["tag_name"]?.jsonPrimitive?.content ?: throw Exception("Failed to get the preview version tag") val releaseInfoJson = URL(WebUI.WEBUI.latestReleaseInfoUrl).readText()
Json.decodeFromString<JsonObject>(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 { private fun getLatestCompatibleVersion(): String {
@@ -285,7 +325,7 @@ object WebInterfaceManager {
} }
val currentServerVersionNumber = extractVersion(BuildConfig.REVISION) 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" } logger.debug { "getLatestCompatibleVersion: webUIChannel= ${serverConfig.webUIChannel}, currentServerVersion= ${BuildConfig.REVISION}, mappingFile= $webUIToServerVersionMappings" }
@@ -311,45 +351,27 @@ object WebInterfaceManager {
throw Exception("No compatible webUI version found") throw Exception("No compatible webUI version found")
} }
fun downloadLatestCompatibleVersion(retryCount: Int = 0): Boolean { fun downloadVersion(version: String) {
val latestCompatibleVersion = getLatestCompatibleVersion() val webUIZip = "${WebUI.WEBUI.baseFileName}-$version.zip"
val webUIZip = "${WebUI.WEBUI.baseFileName}-$latestCompatibleVersion.zip"
val webUIZipPath = "$tmpDir/$webUIZip" 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..." } val log = KotlinLogging.logger("${logger.name} downloadVersion(version= $version, flavor= ${serverConfig.webUIFlavor})")
log.info { "Downloading WebUI 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
}
executeWithRetry(log, { downloadVersionZipFile(webUIZipURL, webUIZipPath) })
File(applicationDirs.webUIRoot).deleteRecursively() File(applicationDirs.webUIRoot).deleteRecursively()
// extract webUI zip // extract webUI zip
logger.info { "downloadLatestCompatibleVersion: Extracting WebUI zip..." } log.info { "Extracting WebUI zip..." }
extractDownload(webUIZipPath, applicationDirs.webUIRoot) extractDownload(webUIZipPath, applicationDirs.webUIRoot)
logger.info { "downloadLatestCompatibleVersion: Extracting WebUI zip Done." } log.info { "Extracting WebUI zip Done." }
return true
} }
private fun downloadVersion(url: String, zipFile: File) { private fun downloadVersionZipFile(url: String, filePath: String) {
val zipFile = File(filePath)
zipFile.delete() zipFile.delete()
val data = ByteArray(1024) val data = ByteArray(1024)
zipFile.outputStream().use { webUIZipFileOut -> zipFile.outputStream().use { webUIZipFileOut ->
@@ -361,7 +383,7 @@ object WebInterfaceManager {
connection.inputStream.buffered().use { inp -> connection.inputStream.buffered().use { inp ->
var totalCount = 0 var totalCount = 0
print("downloadVersion: Download progress: % 00") print("downloadVersionZipFile: Download progress: % 00")
while (true) { while (true) {
val count = inp.read(data, 0, 1024) val count = inp.read(data, 0, 1024)
@@ -377,9 +399,13 @@ object WebInterfaceManager {
webUIZipFileOut.write(data, 0, count) webUIZipFileOut.write(data, 0, count)
} }
println() 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 { private fun isDownloadValid(zipFileName: String, zipFilePath: String): Boolean {
@@ -404,7 +430,7 @@ object WebInterfaceManager {
val latestCompatibleVersion = getLatestCompatibleVersion() val latestCompatibleVersion = getLatestCompatibleVersion()
latestCompatibleVersion != currentVersion latestCompatibleVersion != currentVersion
} catch (e: Exception) { } catch (e: Exception) {
logger.debug { "isUpdateAvailable: check failed due to $e" } logger.warn(e) { "isUpdateAvailable: check failed due to" }
false false
} }
} }