From bbd7e3029821d79ad5737591ef1085dc8e6e0ff6 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 14 Sep 2025 16:32:23 +0200 Subject: [PATCH] Fix/server startup config update failure handling (#1646) * Catch config value migration exception In case the value did not exist in the config a "ConfigException.Missing" exception was thrown which caused the whole migration to fail. Fixes #1645 * Improve config migration logging * Update user config file after config update The user config file gets reset before the update. This could cause the user settings to get lost on the next server start in case something went wrong during the update and the updated config never got saved to the actual file. --- .../xyz/nulldev/ts/config/ConfigManager.kt | 24 +++--- .../suwayomi/tachidesk/server/ServerSetup.kt | 83 ++++++++++++------- 2 files changed, 65 insertions(+), 42 deletions(-) diff --git a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt index acca16e2..6be0b426 100644 --- a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt +++ b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt @@ -120,14 +120,16 @@ open class ConfigManager { } } - fun resetUserConfig(updateInternalConfig: Boolean = true): ConfigDocument { + private fun createConfigDocumentFromReference(): ConfigDocument { val serverConfigFileContent = this::class.java.getResource("/server-reference.conf")?.readText() - val serverConfigDoc = ConfigDocumentFactory.parseString(serverConfigFileContent) - userConfigFile.writeText(serverConfigDoc.render()) + return ConfigDocumentFactory.parseString(serverConfigFileContent) + } - if (updateInternalConfig) { - getUserConfig().entrySet().forEach { internalConfig = internalConfig.withValue(it.key, it.value) } - } + fun resetUserConfig(): ConfigDocument { + val serverConfigDoc = createConfigDocumentFromReference() + + userConfigFile.writeText(serverConfigDoc.render()) + getUserConfig().entrySet().forEach { internalConfig = internalConfig.withValue(it.key, it.value) } return serverConfigDoc } @@ -135,8 +137,9 @@ open class ConfigManager { /** * Makes sure the "UserConfig" is up-to-date. * - * - adds missing settings - * - removes outdated settings + * - Adds missing settings + * - Migrates deprecated settings + * - Removes outdated settings */ fun updateUserConfig(migrate: ConfigDocument.(Config) -> ConfigDocument) { val serverConfig = ConfigFactory.parseResources("server-reference.conf") @@ -149,16 +152,17 @@ open class ConfigManager { } val hasMissingSettings = refKeys.any { !userConfig.hasPath(it) } val hasOutdatedSettings = userConfig.entrySet().any { !refKeys.contains(it.key) && it.key.count { c -> c == '.' } <= 1 } + val isUserConfigOutdated = hasMissingSettings || hasOutdatedSettings if (!isUserConfigOutdated) { return } logger.debug { - "user config is out of date, updating... (missingSettings= $hasMissingSettings, outdatedSettings= $hasOutdatedSettings" + "user config is out of date, updating... (missingSettings= $hasMissingSettings, outdatedSettings= $hasOutdatedSettings)" } - var newUserConfigDoc: ConfigDocument = resetUserConfig(false) + var newUserConfigDoc: ConfigDocument = createConfigDocumentFromReference() userConfig .entrySet() .filter { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt index bc47f56e..fda7e2d2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt @@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.createAppModule import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.local.LocalSource +import io.github.config4k.getValue import io.github.config4k.toConfig import io.github.oshai.kotlinlogging.KotlinLogging import io.javalin.json.JavalinJackson @@ -144,7 +145,7 @@ fun setupLogLevelUpdating( ) } -fun migrateConfig( +fun migrateConfigValue( configDocument: ConfigDocument, config: Config, configKey: String, @@ -168,6 +169,54 @@ fun migrateConfig( return configDocument } +fun migrateConfig( + configDocument: ConfigDocument, + config: Config, +): ConfigDocument { + var updatedConfig = configDocument + + val settingsRequiringMigration = SettingsRegistry.getAll().filterValues { it.deprecated?.replaceWith != null } + settingsRequiringMigration.forEach { (name, data) -> + val configKey = "server.$name" + val toConfigKey = "server.${data.deprecated!!.replaceWith}" + + try { + config.getValue(configKey) + } catch (_: ConfigException) { + // Ignore, no migration required + return@forEach + } + + logger.debug { "Migrating config value: $configKey -> $toConfigKey" } + + try { + if (data.deprecated!!.migrateConfig != null) { + updatedConfig = data.deprecated!!.migrateConfig!!(config.getValue(configKey), updatedConfig) + return@forEach + } + + if (data.deprecated!!.migrateConfigValue != null) { + updatedConfig = + migrateConfigValue( + updatedConfig, + config, + configKey, + toConfigKey, + data.deprecated!!.migrateConfigValue!!, + ) + return@forEach + } + } catch (e: Exception) { + logger.warn(e) { "Failed to migrate config value: $configKey -> $toConfigKey" } + return@forEach + } + + shutdownApp(ExitCode.ConfigMigrationMisconfiguredFailure) + } + + return updatedConfig +} + fun serverModule(applicationDirs: ApplicationDirs): Module = module { single { applicationDirs } @@ -310,37 +359,7 @@ fun applicationSetup() { } } else { // make sure the user config file is up-to-date - GlobalConfigManager.updateUserConfig { config -> - var updatedConfig = this - - val settingsRequiringMigration = SettingsRegistry.getAll().filterValues { it.deprecated?.replaceWith != null } - settingsRequiringMigration.forEach { (name, data) -> - val configKey = "server.$name" - val toConfigKey = "server.${data.deprecated!!.replaceWith}" - - if (data.deprecated!!.migrateConfig != null) { - logger.debug { "Migrating config value: $configKey -> $toConfigKey" } - updatedConfig = data.deprecated!!.migrateConfig!!(config.getValue(configKey), updatedConfig) - return@forEach - } - - if (data.deprecated!!.migrateConfigValue != null) { - updatedConfig = - migrateConfig( - updatedConfig, - config, - configKey, - toConfigKey, - data.deprecated!!.migrateConfigValue!!, - ) - return@forEach - } - - shutdownApp(ExitCode.ConfigMigrationMisconfiguredFailure) - } - - updatedConfig - } + GlobalConfigManager.updateUserConfig { migrateConfig(this, it) } } } catch (e: Exception) { logger.error(e) { "Exception while creating initial server.conf" }