Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Fix/webui setup failure in case bundled webui is missing #625

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
}
Expand All @@ -166,19 +170,29 @@ object WebInterfaceManager {

serverConfig.webUIFlavor = DEFAULT_WEB_UI

val fallbackToBundledVersion = !doDownload()
val fallbackToBundledVersion = !doDownload() { getLatestCompatibleVersion() }
if (!fallbackToBundledVersion) {
return
}
}

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..." }

Expand All @@ -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 {
Expand Down Expand Up @@ -259,10 +277,26 @@ object WebInterfaceManager {
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 {
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) {
""
}
Expand All @@ -274,8 +308,14 @@ object WebInterfaceManager {
}

private fun fetchPreviewVersion(): String {
val releaseInfoJson = URL(WebUI.WEBUI.latestReleaseInfoUrl).readText()
return Json.decodeFromString<JsonObject>(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<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 {
Expand All @@ -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" }

Expand All @@ -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 ->
Expand All @@ -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)

Expand All @@ -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 {
Expand All @@ -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
}
}
Expand Down