From 321fbe22dd2b666291614c70cf11bd4a16b54088 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sat, 12 Aug 2023 17:47:41 +0200 Subject: [PATCH] Feature/listen to server config value changes (#617) * Make server config value changes subscribable * Make server config value changes subscribable - Update usage * Add util functions to listen to server config value changes * Listen to server config value changes - Auto backups * Listen to server config value changes - Auto global update * Listen to server config value changes - WebUI auto updates * Listen to server config value changes - Javalin update ip and port * Listen to server config value changes - Update socks proxy * Listen to server config value changes - Update debug log level * Listen to server config value changes - Update system tray icon * Update config values one at a time In case settings are changed in quick succession it's possible that each setting update reverts the change of the previous changed setting because the internal config hasn't been updated yet. E.g. 1. settingA changed 2. settingB changed 3. settingA updates config file 4. settingB updates config file (internal config hasn't been updated yet with change from settingA) 5. settingA updates internal config (settingA updated) 6. settingB updates internal config (settingB updated, settingA outdated) now settingA is unchanged because settingB reverted its change while updating the config with its new value * Always add log interceptor to OkHttpClient In case debug logs are disabled then the KotlinLogging log level will be set to level > debug and thus, these logs won't get logged * Rename "maxParallelUpdateRequests" to "maxSourcesInParallel" * Use server setting "maxSourcesInParallel" for downloads * Listen to server config value changes - downloads * Always use latest server settings - Browser * Always use latest server settings - folders * [Test] Fix type error --- .../xyz/nulldev/ts/config/ConfigManager.kt | 14 +- .../xyz/nulldev/ts/config/ConfigModule.kt | 4 - .../kanade/tachiyomi/network/NetworkHelper.kt | 17 +-- .../interceptor/CloudflareInterceptor.kt | 4 +- .../graphql/mutations/InfoMutation.kt | 3 +- .../tachidesk/graphql/queries/InfoQuery.kt | 2 +- .../suwayomi/tachidesk/manga/impl/Chapter.kt | 2 +- .../manga/impl/ChapterDownloadHelper.kt | 2 +- .../impl/backup/proto/ProtoBackupExport.kt | 25 +++- .../manga/impl/download/DownloadManager.kt | 23 ++- .../tachidesk/manga/impl/update/Updater.kt | 38 ++++- .../tachidesk/server/ConfigAdapters.kt | 29 ++++ .../suwayomi/tachidesk/server/JavalinSetup.kt | 36 ++++- .../suwayomi/tachidesk/server/ServerConfig.kt | 133 +++++++++++++----- .../suwayomi/tachidesk/server/ServerSetup.kt | 63 ++++++--- .../tachidesk/server/util/AppMutex.kt | 8 +- .../suwayomi/tachidesk/server/util/Browser.kt | 16 ++- .../tachidesk/server/util/SystemTray.kt | 23 ++- .../server/util/WebInterfaceManager.kt | 45 +++--- .../src/main/resources/server-reference.conf | 4 +- .../tachidesk/test/ApplicationTest.kt | 16 +-- .../src/test/resources/server-reference.conf | 4 +- 22 files changed, 368 insertions(+), 143 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/server/ConfigAdapters.kt 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 2aeb83d8..ab9b5576 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 @@ -14,6 +14,8 @@ import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValueFactory import com.typesafe.config.parser.ConfigDocument import com.typesafe.config.parser.ConfigDocumentFactory +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import mu.KotlinLogging import java.io.File @@ -32,6 +34,8 @@ open class ConfigManager { val loadedModules: Map, ConfigModule> get() = generatedModules + private val mutex = Mutex() + /** * Get a config module */ @@ -98,11 +102,13 @@ open class ConfigManager { userConfigFile.writeText(newFileContent) } - fun updateValue(path: String, value: Any) { - val configValue = ConfigValueFactory.fromAnyRef(value) + suspend fun updateValue(path: String, value: Any) { + mutex.withLock { + val configValue = ConfigValueFactory.fromAnyRef(value) - updateUserConfigFile(path, configValue) - internalConfig = internalConfig.withValue(path, configValue) + updateUserConfigFile(path, configValue) + internalConfig = internalConfig.withValue(path, configValue) + } } /** diff --git a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigModule.kt b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigModule.kt index 9005e514..643adbfa 100644 --- a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigModule.kt +++ b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigModule.kt @@ -26,10 +26,6 @@ abstract class SystemPropertyOverridableConfigModule(getConfig: () -> Config, mo /** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */ class SystemPropertyOverrideDelegate(val getConfig: () -> Config, val moduleName: String) { - operator fun setValue(thisRef: R, property: KProperty<*>, value: Any) { - GlobalConfigManager.updateValue("$moduleName.${property.name}", value) - } - inline operator fun getValue(thisRef: R, property: KProperty<*>): T { val configValue: T = getConfig().getValue(thisRef, property) diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt index 068d125a..387521f0 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -20,7 +20,6 @@ import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import mu.KotlinLogging import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor -import suwayomi.tachidesk.server.serverConfig import java.net.CookieHandler import java.net.CookieManager import java.net.CookiePolicy @@ -53,18 +52,16 @@ class NetworkHelper(context: Context) { .callTimeout(2, TimeUnit.MINUTES) .addInterceptor(UserAgentInterceptor()) - if (serverConfig.debugLogsEnabled) { - val httpLoggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger { - val logger = KotlinLogging.logger { } + val httpLoggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger { + val logger = KotlinLogging.logger { } - override fun log(message: String) { - logger.debug { message } - } - }).apply { - level = HttpLoggingInterceptor.Level.BASIC + override fun log(message: String) { + logger.debug { message } } - builder.addInterceptor(httpLoggingInterceptor) + }).apply { + level = HttpLoggingInterceptor.Level.BASIC } + builder.addInterceptor(httpLoggingInterceptor) // when (preferences.dohProvider()) { // PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index 9960720d..421eecc9 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -87,8 +87,8 @@ object CFClearance { LaunchOptions() .setHeadless(false) .apply { - if (serverConfig.socksProxyEnabled) { - setProxy("socks5://${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") + if (serverConfig.socksProxyEnabled.value) { + setProxy("socks5://${serverConfig.socksProxyHost.value}:${serverConfig.socksProxyPort.value}") } } ).use { browser -> diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt index 76e57ba9..f484cb13 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt @@ -3,7 +3,6 @@ package suwayomi.tachidesk.graphql.mutations import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING -import suwayomi.tachidesk.graphql.types.UpdateState.FINISHED import suwayomi.tachidesk.graphql.types.UpdateState.STOPPED import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus @@ -37,7 +36,7 @@ class InfoMutation { input.clientMutationId, WebUIUpdateStatus( info = WebUIUpdateInfo( - channel = serverConfig.webUIChannel, + channel = serverConfig.webUIChannel.value, tag = version, updateAvailable ), diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt index c00d68fc..45ed95fc 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt @@ -55,7 +55,7 @@ class InfoQuery { return future { val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable() WebUIUpdateInfo( - channel = serverConfig.webUIChannel, + channel = serverConfig.webUIChannel.value, tag = version, updateAvailable ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt index 391e4b28..3637a71f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt @@ -210,7 +210,7 @@ object Chapter { val wasInitialFetch = currentNumberOfChapters == 0 // make sure to ignore initial fetch - val downloadNewChapters = serverConfig.autoDownloadNewChapters && !wasInitialFetch && areNewChaptersAvailable + val downloadNewChapters = serverConfig.autoDownloadNewChapters.value && !wasInitialFetch && areNewChaptersAvailable if (!downloadNewChapters) { return } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt index 907a217a..4c61a6c0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt @@ -35,7 +35,7 @@ object ChapterDownloadHelper { val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId)) val cbzFile = File(getChapterCbzPath(mangaId, chapterId)) if (cbzFile.exists()) return ArchiveProvider(mangaId, chapterId) - if (!chapterFolder.exists() && serverConfig.downloadAsCbz) return ArchiveProvider(mangaId, chapterId) + if (!chapterFolder.exists() && serverConfig.downloadAsCbz.value) return ArchiveProvider(mangaId, chapterId) return FolderProvider(mangaId, chapterId) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt index 474a72ee..2ebb4aa7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt @@ -8,6 +8,7 @@ package suwayomi.tachidesk.manga.impl.backup.proto * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import eu.kanade.tachiyomi.source.model.UpdateStrategy +import kotlinx.coroutines.flow.combine import mu.KotlinLogging import okio.buffer import okio.gzip @@ -53,10 +54,22 @@ object ProtoBackupExport : ProtoBackupBase() { private const val lastAutomatedBackupKey = "lastAutomatedBackupKey" private val preferences = Preferences.userNodeForPackage(ProtoBackupExport::class.java) + init { + serverConfig.subscribeTo( + combine(serverConfig.backupInterval, serverConfig.backupTime) { interval, timeOfDay -> + Pair( + interval, + timeOfDay + ) + }, + ::scheduleAutomatedBackupTask + ) + } + fun scheduleAutomatedBackupTask() { HAScheduler.descheduleCron(backupSchedulerJobId) - val areAutomatedBackupsDisabled = serverConfig.backupInterval == 0 + val areAutomatedBackupsDisabled = serverConfig.backupInterval.value == 0 if (areAutomatedBackupsDisabled) { return } @@ -67,10 +80,10 @@ object ProtoBackupExport : ProtoBackupBase() { preferences.putLong(lastAutomatedBackupKey, System.currentTimeMillis()) } - val (hour, minute) = serverConfig.backupTime.split(":").map { it.toInt() } + val (hour, minute) = serverConfig.backupTime.value.split(":").map { it.toInt() } val backupHour = hour.coerceAtLeast(0).coerceAtMost(23) val backupMinute = minute.coerceAtLeast(0).coerceAtMost(59) - val backupInterval = serverConfig.backupInterval.days.coerceAtLeast(1.days) + val backupInterval = serverConfig.backupInterval.value.days.coerceAtLeast(1.days) // trigger last backup in case the server wasn't running on the scheduled time val lastAutomatedBackup = preferences.getLong(lastAutomatedBackupKey, System.currentTimeMillis()) @@ -105,9 +118,9 @@ object ProtoBackupExport : ProtoBackupBase() { } private fun cleanupAutomatedBackups() { - logger.debug { "Cleanup automated backups (ttl= ${serverConfig.backupTTL})" } + logger.debug { "Cleanup automated backups (ttl= ${serverConfig.backupTTL.value})" } - val isCleanupDisabled = serverConfig.backupTTL == 0 + val isCleanupDisabled = serverConfig.backupTTL.value == 0 if (isCleanupDisabled) { return } @@ -133,7 +146,7 @@ object ProtoBackupExport : ProtoBackupBase() { val lastAccessTime = file.lastModified() val isTTLReached = - System.currentTimeMillis() - lastAccessTime >= serverConfig.backupTTL.days.coerceAtLeast(1.days).inWholeMilliseconds + System.currentTimeMillis() - lastAccessTime >= serverConfig.backupTTL.value.days.coerceAtLeast(1.days).inWholeMilliseconds if (isTTLReached) { file.delete() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt index ca5d885f..4d44c9f6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt @@ -42,6 +42,7 @@ import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.toDataClass +import suwayomi.tachidesk.server.serverConfig import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.concurrent.ConcurrentHashMap @@ -51,8 +52,6 @@ import kotlin.time.Duration.Companion.seconds private val logger = KotlinLogging.logger {} -private const val MAX_SOURCES_IN_PARAllEL = 5 - object DownloadManager { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val clients = ConcurrentHashMap() @@ -169,6 +168,22 @@ object DownloadManager { private val downloaderWatch = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) init { + serverConfig.subscribeTo(serverConfig.maxSourcesInParallel, { maxSourcesInParallel -> + val runningDownloaders = downloaders.values.filter { it.isActive } + var downloadersToStop = runningDownloaders.size - maxSourcesInParallel + + logger.debug { "Max sources in parallel changed to $maxSourcesInParallel (running downloaders ${runningDownloaders.size})" } + + if (downloadersToStop > 0) { + runningDownloaders.takeWhile { + it.stop() + --downloadersToStop > 0 + } + } else { + downloaderWatch.emit(Unit) + } + }) + scope.launch { downloaderWatch.sample(1.seconds).collect { val runningDownloaders = downloaders.values.filter { it.isActive } @@ -176,14 +191,14 @@ object DownloadManager { logger.info { "Running: ${runningDownloaders.size}, Queued: ${availableDownloads.size}, Failed: ${downloadQueue.size - availableDownloads.size}" } - if (runningDownloaders.size < MAX_SOURCES_IN_PARAllEL) { + if (runningDownloaders.size < serverConfig.maxSourcesInParallel.value) { availableDownloads.asSequence() .map { it.manga.sourceId } .distinct() .minus( runningDownloaders.map { it.sourceId }.toSet() ) - .take(MAX_SOURCES_IN_PARAllEL - runningDownloaders.size) + .take(serverConfig.maxSourcesInParallel.value - runningDownloaders.size) .map { getDownloader(it) } .forEach { it.start() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt index 4b23ebf0..6883267a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt @@ -33,6 +33,7 @@ import suwayomi.tachidesk.util.HAScheduler import java.util.Date import java.util.concurrent.ConcurrentHashMap import java.util.prefs.Preferences +import kotlin.math.absoluteValue import kotlin.time.Duration.Companion.hours class Updater : IUpdater { @@ -45,13 +46,36 @@ class Updater : IUpdater { private val tracker = ConcurrentHashMap() private val updateChannels = ConcurrentHashMap>() - private val semaphore = Semaphore(serverConfig.maxParallelUpdateRequests) + private var maxSourcesInParallel = 20 // max permits, necessary to be set to be able to release up to 20 permits + private val semaphore = Semaphore(maxSourcesInParallel) private val lastAutomatedUpdateKey = "lastAutomatedUpdateKey" private val preferences = Preferences.userNodeForPackage(Updater::class.java) private var currentUpdateTaskId = "" + init { + serverConfig.subscribeTo(serverConfig.globalUpdateInterval, ::scheduleUpdateTask) + serverConfig.subscribeTo( + serverConfig.maxSourcesInParallel, + { value -> + val newMaxPermits = value.coerceAtLeast(1).coerceAtMost(20) + val permitDifference = maxSourcesInParallel - newMaxPermits + maxSourcesInParallel = newMaxPermits + + val addMorePermits = permitDifference < 0 + for (i in 1..permitDifference.absoluteValue) { + if (addMorePermits) { + semaphore.release() + } else { + semaphore.acquire() + } + } + }, + ignoreInitialValue = false + ) + } + private fun autoUpdateTask() { val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0) preferences.putLong(lastAutomatedUpdateKey, System.currentTimeMillis()) @@ -61,19 +85,19 @@ class Updater : IUpdater { return } - logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" } + logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval.value}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" } addCategoriesToUpdateQueue(Category.getCategoryList(), clear = true, forceAll = false) } fun scheduleUpdateTask() { HAScheduler.deschedule(currentUpdateTaskId) - val isAutoUpdateDisabled = serverConfig.globalUpdateInterval == 0.0 + val isAutoUpdateDisabled = serverConfig.globalUpdateInterval.value == 0.0 if (isAutoUpdateDisabled) { return } - val updateInterval = serverConfig.globalUpdateInterval.hours.coerceAtLeast(6.hours).inWholeMilliseconds + val updateInterval = serverConfig.globalUpdateInterval.value.hours.coerceAtLeast(6.hours).inWholeMilliseconds val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0) val timeToNextExecution = (updateInterval - (System.currentTimeMillis() - lastAutomatedUpdate)).mod(updateInterval) @@ -150,9 +174,9 @@ class Updater : IUpdater { val mangasToUpdate = categoriesToUpdateMangas .asSequence() .filter { it.updateStrategy == UpdateStrategy.ALWAYS_UPDATE } - .filter { if (serverConfig.excludeUnreadChapters) { (it.unreadCount ?: 0L) == 0L } else true } - .filter { if (serverConfig.excludeNotStarted) { it.lastReadAt != null } else true } - .filter { if (serverConfig.excludeCompleted) { it.status != MangaStatus.COMPLETED.name } else true } + .filter { if (serverConfig.excludeUnreadChapters.value) { (it.unreadCount ?: 0L) == 0L } else true } + .filter { if (serverConfig.excludeNotStarted.value) { it.lastReadAt != null } else true } + .filter { if (serverConfig.excludeCompleted.value) { it.status != MangaStatus.COMPLETED.name } else true } .filter { forceAll || !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } } .toList() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ConfigAdapters.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ConfigAdapters.kt new file mode 100644 index 00000000..e7668f8d --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ConfigAdapters.kt @@ -0,0 +1,29 @@ +package suwayomi.tachidesk.server + +interface ConfigAdapter { + fun toType(configValue: String): T +} + +object StringConfigAdapter : ConfigAdapter { + override fun toType(configValue: String): String { + return configValue + } +} + +object IntConfigAdapter : ConfigAdapter { + override fun toType(configValue: String): Int { + return configValue.toInt() + } +} + +object BooleanConfigAdapter : ConfigAdapter { + override fun toType(configValue: String): Boolean { + return configValue.toBoolean() + } +} + +object DoubleConfigAdapter : ConfigAdapter { + override fun toType(configValue: String): Double { + return configValue.toDouble() + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index be26cb0a..9e52f95c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -18,9 +18,12 @@ import io.swagger.v3.oas.models.info.Info import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.future.future import kotlinx.coroutines.runBlocking import mu.KotlinLogging +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.ServerConnector import org.kodein.di.DI import org.kodein.di.conf.global import org.kodein.di.instance @@ -46,27 +49,48 @@ object JavalinSetup { } fun javalinSetup() { + val server = Server() + val connector = ServerConnector(server).apply { + host = serverConfig.ip.value + port = serverConfig.port.value + } + server.addConnector(connector) + + serverConfig.subscribeTo(combine(serverConfig.ip, serverConfig.port) { ip, port -> Pair(ip, port) }, { (newIp, newPort) -> + val oldIp = connector.host + val oldPort = connector.port + + connector.host = newIp + connector.port = newPort + connector.stop() + connector.start() + + logger.info { "Server ip and/or port changed from $oldIp:$oldPort to $newIp:$newPort " } + }) + val app = Javalin.create { config -> - if (serverConfig.webUIEnabled) { + if (serverConfig.webUIEnabled.value) { runBlocking { WebInterfaceManager.setupWebUI() } - logger.info { "Serving web static files for ${serverConfig.webUIFlavor}" } + logger.info { "Serving web static files for ${serverConfig.webUIFlavor.value}" } config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL) config.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL) config.registerPlugin(OpenApiPlugin(getOpenApiOptions())) } + config.server { server } + config.enableCorsForAllOrigins() config.accessManager { handler, ctx, _ -> fun credentialsValid(): Boolean { val (username, password) = ctx.basicAuthCredentials() - return username == serverConfig.basicAuthUsername && password == serverConfig.basicAuthPassword + return username == serverConfig.basicAuthUsername.value && password == serverConfig.basicAuthPassword.value } - if (serverConfig.basicAuthEnabled && !(ctx.basicAuthCredentialsExist() && credentialsValid())) { + if (serverConfig.basicAuthEnabled.value && !(ctx.basicAuthCredentialsExist() && credentialsValid())) { ctx.header("WWW-Authenticate", "Basic") ctx.status(401).json("Unauthorized") } else { @@ -75,11 +99,11 @@ object JavalinSetup { } }.events { event -> event.serverStarted { - if (serverConfig.initialOpenInBrowserEnabled) { + if (serverConfig.initialOpenInBrowserEnabled.value) { Browser.openInBrowser() } } - }.start(serverConfig.ip, serverConfig.port) + }.start() // when JVM is prompted to shutdown, stop javalin gracefully Runtime.getRuntime().addShutdownHook( diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt index c3f0ce01..c699a732 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt @@ -8,58 +8,127 @@ package suwayomi.tachidesk.server * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import com.typesafe.config.Config +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule -import xyz.nulldev.ts.config.debugLogsEnabled +import kotlin.reflect.KProperty + +val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private const val MODULE_NAME = "server" -class ServerConfig(getConfig: () -> Config, moduleName: String = MODULE_NAME) : SystemPropertyOverridableConfigModule(getConfig, moduleName) { - var ip: String by overridableConfig - var port: Int by overridableConfig +class ServerConfig(getConfig: () -> Config, val moduleName: String = MODULE_NAME) : SystemPropertyOverridableConfigModule(getConfig, moduleName) { + inner class OverrideConfigValue(private val configAdapter: ConfigAdapter) { + private var flow: MutableStateFlow? = null + + operator fun getValue(thisRef: ServerConfig, property: KProperty<*>): MutableStateFlow { + if (flow != null) { + return flow!! + } + + val value = configAdapter.toType(overridableConfig.getValue(thisRef, property)) + + val stateFlow = MutableStateFlow(value) + flow = stateFlow + + stateFlow.drop(1).distinctUntilChanged().onEach { + GlobalConfigManager.updateValue("$moduleName.${property.name}", it as Any) + }.launchIn(mutableConfigValueScope) + + return stateFlow + } + } + + val ip: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val port: MutableStateFlow by OverrideConfigValue(IntConfigAdapter) // proxy - var socksProxyEnabled: Boolean by overridableConfig - var socksProxyHost: String by overridableConfig - var socksProxyPort: String by overridableConfig + val socksProxyEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val socksProxyHost: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val socksProxyPort: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) // webUI - var webUIEnabled: Boolean by overridableConfig - var webUIFlavor: String by overridableConfig - var initialOpenInBrowserEnabled: Boolean by overridableConfig - var webUIInterface: String by overridableConfig - var electronPath: String by overridableConfig - var webUIChannel: String by overridableConfig - var webUIUpdateCheckInterval: Double by overridableConfig + val webUIEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val webUIFlavor: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val initialOpenInBrowserEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val webUIInterface: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val electronPath: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val webUIChannel: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val webUIUpdateCheckInterval: MutableStateFlow by OverrideConfigValue(DoubleConfigAdapter) // downloader - var downloadAsCbz: Boolean by overridableConfig - var downloadsPath: String by overridableConfig - var autoDownloadNewChapters: Boolean by overridableConfig + val downloadAsCbz: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val downloadsPath: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val autoDownloadNewChapters: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + + // requests + val maxSourcesInParallel: MutableStateFlow by OverrideConfigValue(IntConfigAdapter) // updater - var maxParallelUpdateRequests: Int by overridableConfig - var excludeUnreadChapters: Boolean by overridableConfig - var excludeNotStarted: Boolean by overridableConfig - var excludeCompleted: Boolean by overridableConfig - var globalUpdateInterval: Double by overridableConfig + val excludeUnreadChapters: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val excludeNotStarted: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val excludeCompleted: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val globalUpdateInterval: MutableStateFlow by OverrideConfigValue(DoubleConfigAdapter) // Authentication - var basicAuthEnabled: Boolean by overridableConfig - var basicAuthUsername: String by overridableConfig - var basicAuthPassword: String by overridableConfig + val basicAuthEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val basicAuthUsername: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val basicAuthPassword: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) // misc - var debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config) - var systemTrayEnabled: Boolean by overridableConfig + val debugLogsEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val systemTrayEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) // backup - var backupPath: String by overridableConfig - var backupTime: String by overridableConfig - var backupInterval: Int by overridableConfig - var backupTTL: Int by overridableConfig + val backupPath: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val backupTime: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val backupInterval: MutableStateFlow by OverrideConfigValue(IntConfigAdapter) + val backupTTL: MutableStateFlow by OverrideConfigValue(IntConfigAdapter) // local source - var localSourcePath: String by overridableConfig + val localSourcePath: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + + fun subscribeTo(flow: Flow, onChange: suspend (value: T) -> Unit, ignoreInitialValue: Boolean = true) { + val actualFlow = if (ignoreInitialValue) { + flow.drop(1) + } else { + flow + } + + val sharedFlow = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + actualFlow.distinctUntilChanged().onEach { sharedFlow.emit(it) }.launchIn(mutableConfigValueScope) + sharedFlow.onEach { onChange(it) }.launchIn(mutableConfigValueScope) + } + + fun subscribeTo(flow: Flow, onChange: suspend () -> Unit, ignoreInitialValue: Boolean = true) { + subscribeTo(flow, { _ -> onChange() }, ignoreInitialValue) + } + + fun subscribeTo( + mutableStateFlow: MutableStateFlow, + onChange: suspend (value: T) -> Unit, + ignoreInitialValue: Boolean = true + ) { + subscribeTo(mutableStateFlow.asStateFlow(), onChange, ignoreInitialValue) + } + + fun subscribeTo( + mutableStateFlow: MutableStateFlow, + onChange: suspend () -> Unit, + ignoreInitialValue: Boolean = true + ) { + subscribeTo(mutableStateFlow.asStateFlow(), { _ -> onChange() }, ignoreInitialValue) + } companion object { fun register(getConfig: () -> Config) = ServerConfig({ getConfig().getConfig(MODULE_NAME) }) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt index f504afb3..d5f33091 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt @@ -7,11 +7,13 @@ package suwayomi.tachidesk.server * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import ch.qos.logback.classic.Level import com.typesafe.config.ConfigRenderOptions import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.source.local.LocalSource import io.javalin.plugin.json.JavalinJackson import io.javalin.plugin.json.JsonMapper +import kotlinx.coroutines.flow.combine import kotlinx.serialization.json.Json import mu.KotlinLogging import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -26,13 +28,14 @@ import suwayomi.tachidesk.manga.impl.update.Updater import suwayomi.tachidesk.manga.impl.util.lang.renameTo import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex -import suwayomi.tachidesk.server.util.SystemTray.systemTray +import suwayomi.tachidesk.server.util.SystemTray import xyz.nulldev.androidcompat.AndroidCompat import xyz.nulldev.androidcompat.AndroidCompatInitializer import xyz.nulldev.ts.config.ApplicationRootDir import xyz.nulldev.ts.config.ConfigKodeinModule import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.initLoggerConfig +import xyz.nulldev.ts.config.setLogLevel import java.io.File import java.security.Security import java.util.Locale @@ -43,29 +46,25 @@ class ApplicationDirs( val dataRoot: String = ApplicationRootDir, val tempRoot: String = "${System.getProperty("java.io.tmpdir")}/Tachidesk" ) { - val cacheRoot = System.getProperty("java.io.tmpdir") + "/tachidesk" val extensionsRoot = "$dataRoot/extensions" - val downloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" } - val localMangaRoot = serverConfig.localSourcePath.ifBlank { "$dataRoot/local" } + val downloadsRoot get() = serverConfig.downloadsPath.value.ifBlank { "$dataRoot/downloads" } + val localMangaRoot get() = serverConfig.localSourcePath.value.ifBlank { "$dataRoot/local" } val webUIRoot = "$dataRoot/webUI" - val automatedBackupRoot = serverConfig.backupPath.ifBlank { "$dataRoot/backups" } + val automatedBackupRoot get() = serverConfig.backupPath.value.ifBlank { "$dataRoot/backups" } val tempThumbnailCacheRoot = "$tempRoot/thumbnails" val tempMangaCacheRoot = "$tempRoot/manga-cache" - val thumbnailDownloadsRoot = "$downloadsRoot/thumbnails" - val mangaDownloadsRoot = "$downloadsRoot/mangas" + val thumbnailDownloadsRoot get() = "$downloadsRoot/thumbnails" + val mangaDownloadsRoot get() = "$downloadsRoot/mangas" } val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() } -val systemTrayInstance by lazy { systemTray() } - val androidCompat by lazy { AndroidCompat() } fun applicationSetup() { - Thread.setDefaultUncaughtExceptionHandler { - _, throwable -> + Thread.setDefaultUncaughtExceptionHandler { _, throwable -> KotlinLogging.logger { }.error(throwable) { "unhandled exception" } } @@ -74,6 +73,14 @@ fun applicationSetup() { ServerConfig.register { GlobalConfigManager.config } ) + serverConfig.subscribeTo(serverConfig.debugLogsEnabled, { debugLogsEnabled -> + if (debugLogsEnabled) { + setLogLevel(Level.DEBUG) + } else { + setLogLevel(Level.INFO) + } + }) + // Application dirs val applicationDirs = ApplicationDirs() @@ -164,13 +171,17 @@ fun applicationSetup() { LocalSource.register() // create system tray - if (serverConfig.systemTrayEnabled) { + serverConfig.subscribeTo(serverConfig.systemTrayEnabled, { systemTrayEnabled -> try { - systemTrayInstance + if (systemTrayEnabled) { + SystemTray.create() + } else { + SystemTray.remove() + } } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error e.printStackTrace() } - } + }, ignoreInitialValue = false) // Disable jetty's logging System.setProperty("org.eclipse.jetty.util.log.announce", "false") @@ -178,11 +189,25 @@ fun applicationSetup() { System.setProperty("org.eclipse.jetty.LEVEL", "OFF") // socks proxy settings - if (serverConfig.socksProxyEnabled) { - System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost - System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort - logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") - } + serverConfig.subscribeTo( + combine( + serverConfig.socksProxyEnabled, + serverConfig.socksProxyHost, + serverConfig.socksProxyPort + ) { proxyEnabled, proxyHost, proxyPort -> + Triple(proxyEnabled, proxyHost, proxyPort) + }, + { (proxyEnabled, proxyHost, proxyPort) -> + logger.info("Socks Proxy changed - enabled= $proxyEnabled, proxy= $proxyHost:$proxyPort") + if (proxyEnabled) { + System.getProperties()["socksProxyHost"] = proxyHost + System.getProperties()["socksProxyPort"] = proxyPort + } else { + System.getProperties()["socksProxyHost"] = "" + System.getProperties()["socksProxyPort"] = "" + } + } + ) // AES/CBC/PKCS7Padding Cypher provider for zh.copymanga Security.addProvider(BouncyCastleProvider()) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/AppMutex.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/AppMutex.kt index 7f0f3671..4b1120ae 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/AppMutex.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/AppMutex.kt @@ -31,7 +31,7 @@ object AppMutex { OtherApplicationRunning(2) } - private val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip + private val appIP = if (serverConfig.ip.value == "0.0.0.0") "127.0.0.1" else serverConfig.ip.value private val jsonMapper by DI.global.instance() @@ -41,7 +41,7 @@ object AppMutex { .build() val request = Builder() - .url("http://$appIP:${serverConfig.port}/api/v1/settings/about/") + .url("http://$appIP:${serverConfig.port.value}/api/v1/settings/about/") .build() val response = try { @@ -64,7 +64,7 @@ object AppMutex { logger.info("Mutex status is clear, Resuming startup.") } AppMutexState.TachideskInstanceRunning -> { - logger.info("Another instance of Tachidesk is running on $appIP:${serverConfig.port}") + logger.info("Another instance of Tachidesk is running on $appIP:${serverConfig.port.value}") logger.info("Probably user thought tachidesk is closed so, opening webUI in browser again.") openInBrowser() @@ -74,7 +74,7 @@ object AppMutex { shutdownApp(MutexCheckFailedTachideskRunning) } AppMutexState.OtherApplicationRunning -> { - logger.error("A non Tachidesk application is running on $appIP:${serverConfig.port}, aborting startup.") + logger.error("A non Tachidesk application is running on $appIP:${serverConfig.port.value}, aborting startup.") shutdownApp(MutexCheckFailedAnotherAppRunning) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/Browser.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/Browser.kt index e055e64e..c87f3de4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/Browser.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/Browser.kt @@ -11,16 +11,20 @@ import dorkbox.desktop.Desktop import suwayomi.tachidesk.server.serverConfig object Browser { - private val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip - private val appBaseUrl = "http://$appIP:${serverConfig.port}" - private val electronInstances = mutableListOf() + private fun getAppBaseUrl(): String { + val appIP = if (serverConfig.ip.value == "0.0.0.0") "127.0.0.1" else serverConfig.ip.value + return "http://$appIP:${serverConfig.port.value}" + } + fun openInBrowser() { - if (serverConfig.webUIEnabled) { - if (serverConfig.webUIInterface == ("electron")) { + if (serverConfig.webUIEnabled.value) { + val appBaseUrl = getAppBaseUrl() + + if (serverConfig.webUIInterface.value == ("electron")) { try { - val electronPath = serverConfig.electronPath + val electronPath = serverConfig.electronPath.value electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start()) } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error e.printStackTrace() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/SystemTray.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/SystemTray.kt index 978f6307..6bbc70be 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/SystemTray.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/SystemTray.kt @@ -17,10 +17,16 @@ import suwayomi.tachidesk.server.util.Browser.openInBrowser import suwayomi.tachidesk.server.util.ExitCode.Success object SystemTray { - fun systemTray(): SystemTray? { - try { + private var instance: SystemTray? = null + + fun create() { + instance = try { // ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java - SystemTray.DEBUG = serverConfig.debugLogsEnabled + serverConfig.subscribeTo( + serverConfig.debugLogsEnabled, + { debugLogsEnabled -> SystemTray.DEBUG = debugLogsEnabled }, + ignoreInitialValue = false + ) CacheUtil.clear(BuildConfig.NAME) @@ -28,7 +34,7 @@ object SystemTray { SystemTray.FORCE_TRAY_TYPE = SystemTray.TrayType.Awt } - val systemTray = SystemTray.get(BuildConfig.NAME) ?: return null + val systemTray = SystemTray.get(BuildConfig.NAME) val mainMenu = systemTray.menu mainMenu.add( @@ -51,10 +57,15 @@ object SystemTray { } ) - return systemTray + systemTray } catch (e: Exception) { e.printStackTrace() - return null + null } } + + fun remove() { + instance?.remove() + instance = null + } } 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 cafd2892..856e7341 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt @@ -16,11 +16,11 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject @@ -70,7 +70,7 @@ enum class WebUIChannel { companion object { fun doesConfigChannelEqual(channel: WebUIChannel): Boolean { - return serverConfig.webUIChannel.equals(channel.toString(), true) + return serverConfig.webUIChannel.value.equals(channel.toString(), true) } } } @@ -112,7 +112,7 @@ object WebInterfaceManager { SharingStarted.Eagerly, WebUIUpdateStatus( info = WebUIUpdateInfo( - channel = serverConfig.webUIChannel, + channel = serverConfig.webUIChannel.value, tag = "", updateAvailable = false ), @@ -122,27 +122,36 @@ object WebInterfaceManager { ) init { - scheduleWebUIUpdateCheck() + serverConfig.subscribeTo( + combine(serverConfig.webUIUpdateCheckInterval, serverConfig.webUIFlavor) { interval, flavor -> + Pair( + interval, + flavor + ) + }, + ::scheduleWebUIUpdateCheck, + ignoreInitialValue = false + ) } private fun isAutoUpdateEnabled(): Boolean { - return serverConfig.webUIUpdateCheckInterval.toInt() != 0 + return serverConfig.webUIUpdateCheckInterval.value.toInt() != 0 } private fun scheduleWebUIUpdateCheck() { HAScheduler.descheduleCron(currentUpdateTaskId) - val isAutoUpdateDisabled = !isAutoUpdateEnabled() || serverConfig.webUIFlavor == "Custom" + val isAutoUpdateDisabled = !isAutoUpdateEnabled() || serverConfig.webUIFlavor.value == "Custom" if (isAutoUpdateDisabled) { return } - val updateInterval = serverConfig.webUIUpdateCheckInterval.hours.coerceAtLeast(1.hours).coerceAtMost(23.hours) + val updateInterval = serverConfig.webUIUpdateCheckInterval.value.hours.coerceAtLeast(1.hours).coerceAtMost(23.hours) val lastAutomatedUpdate = preferences.getLong(lastWebUIUpdateCheckKey, System.currentTimeMillis()) val task = { logger.debug { - "Checking for webUI update (channel= ${serverConfig.webUIChannel}, interval= ${serverConfig.webUIUpdateCheckInterval}h, lastAutomatedUpdate= ${ + "Checking for webUI update (channel= ${serverConfig.webUIChannel.value}, interval= ${serverConfig.webUIUpdateCheckInterval.value}h, lastAutomatedUpdate= ${ Date( lastAutomatedUpdate ) @@ -165,14 +174,14 @@ object WebInterfaceManager { } suspend fun setupWebUI() { - if (serverConfig.webUIFlavor == "Custom") { + if (serverConfig.webUIFlavor.value == "Custom") { return } if (doesLocalWebUIExist(applicationDirs.webUIRoot)) { val currentVersion = getLocalVersion() - logger.info { "setupWebUI: found webUI files - flavor= ${serverConfig.webUIFlavor}, version= $currentVersion" } + logger.info { "setupWebUI: found webUI files - flavor= ${serverConfig.webUIFlavor.value}, version= $currentVersion" } if (!isLocalWebUIValid(applicationDirs.webUIRoot)) { doInitialSetup() @@ -186,7 +195,7 @@ object WebInterfaceManager { // check if the bundled webUI version is a newer version than the current used version // this could be the case in case no compatible webUI version is available and a newer server version was installed val shouldUpdateToBundledVersion = - serverConfig.webUIFlavor == DEFAULT_WEB_UI && extractVersion(getLocalVersion()) < extractVersion( + serverConfig.webUIFlavor.value == DEFAULT_WEB_UI && extractVersion(getLocalVersion()) < extractVersion( BuildConfig.WEBUI_TAG ) if (shouldUpdateToBundledVersion) { @@ -232,10 +241,10 @@ object WebInterfaceManager { return } - if (serverConfig.webUIFlavor != DEFAULT_WEB_UI) { + if (serverConfig.webUIFlavor.value != DEFAULT_WEB_UI) { logger.warn { "doInitialSetup: fallback to default webUI \"$DEFAULT_WEB_UI\"" } - serverConfig.webUIFlavor = DEFAULT_WEB_UI + serverConfig.webUIFlavor.value = DEFAULT_WEB_UI val fallbackToBundledVersion = !doDownload() { getLatestCompatibleVersion() } if (!fallbackToBundledVersion) { @@ -287,11 +296,11 @@ object WebInterfaceManager { val localVersion = getLocalVersion() if (!isUpdateAvailable(localVersion).second) { - logger.debug { "checkForUpdate(${serverConfig.webUIFlavor}, $localVersion): local version is the latest one" } + logger.debug { "checkForUpdate(${serverConfig.webUIFlavor.value}, $localVersion): local version is the latest one" } return } - logger.info { "checkForUpdate(${serverConfig.webUIFlavor}, $localVersion): An update is available, starting download..." } + logger.info { "checkForUpdate(${serverConfig.webUIFlavor.value}, $localVersion): An update is available, starting download..." } try { downloadVersion(getLatestCompatibleVersion()) } catch (e: Exception) { @@ -416,7 +425,7 @@ object WebInterfaceManager { val currentServerVersionNumber = extractVersion(BuildConfig.REVISION) val webUIToServerVersionMappings = fetchServerMappingFile() - logger.debug { "getLatestCompatibleVersion: webUIChannel= ${serverConfig.webUIChannel}, currentServerVersion= ${BuildConfig.REVISION}, mappingFile= $webUIToServerVersionMappings" } + logger.debug { "getLatestCompatibleVersion: webUIChannel= ${serverConfig.webUIChannel.value}, currentServerVersion= ${BuildConfig.REVISION}, mappingFile= $webUIToServerVersionMappings" } for (i in 0 until webUIToServerVersionMappings.size) { val webUIToServerVersionEntry = webUIToServerVersionMappings[i].jsonObject @@ -446,7 +455,7 @@ object WebInterfaceManager { notifyFlow.emit( WebUIUpdateStatus( info = WebUIUpdateInfo( - channel = serverConfig.webUIChannel, + channel = serverConfig.webUIChannel.value, tag = version, updateAvailable = true ), @@ -472,7 +481,7 @@ object WebInterfaceManager { val webUIZipURL = "${getDownloadUrlFor(version)}/$webUIZip" val log = - KotlinLogging.logger("${logger.name} downloadVersion(version= $version, flavor= ${serverConfig.webUIFlavor})") + KotlinLogging.logger("${logger.name} downloadVersion(version= $version, flavor= ${serverConfig.webUIFlavor.value})") log.info { "Downloading WebUI zip from the Internet..." } executeWithRetry(log, { diff --git a/server/src/main/resources/server-reference.conf b/server/src/main/resources/server-reference.conf index 18df8771..db38de15 100644 --- a/server/src/main/resources/server-reference.conf +++ b/server/src/main/resources/server-reference.conf @@ -21,8 +21,10 @@ server.downloadAsCbz = false server.downloadsPath = "" server.autoDownloadNewChapters = false # if new chapters that have been retrieved should get automatically downloaded +# requests +server.maxSourcesInParallel = 6 # range: 1 <= n <= 20 - default: 6 - sets how many sources can do requests (updates, downloads) in parallel. updates/downloads are grouped by source and all mangas of a source are updated/downloaded synchronously + # updater -server.maxParallelUpdateRequests = 10 # sets how many sources can be updated in parallel. updates are grouped by source and all mangas of a source are updated synchronously server.excludeUnreadChapters = true server.excludeNotStarted = true server.excludeCompleted = true diff --git a/server/src/test/kotlin/suwayomi/tachidesk/test/ApplicationTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/test/ApplicationTest.kt index f723e0ff..cde63aa2 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/test/ApplicationTest.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/test/ApplicationTest.kt @@ -26,8 +26,8 @@ import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.androidCompat import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.serverConfig -import suwayomi.tachidesk.server.systemTrayInstance import suwayomi.tachidesk.server.util.AppMutex +import suwayomi.tachidesk.server.util.SystemTray import xyz.nulldev.androidcompat.AndroidCompatInitializer import xyz.nulldev.ts.config.CONFIG_PREFIX import xyz.nulldev.ts.config.ConfigKodeinModule @@ -83,7 +83,7 @@ open class ApplicationTest { // register Tachidesk's config which is dubbed "ServerConfig" GlobalConfigManager.registerModule( - ServerConfig.register(GlobalConfigManager.config) + ServerConfig.register { GlobalConfigManager.config } ) // Make sure only one instance of the app is running @@ -125,9 +125,9 @@ open class ApplicationTest { } // create system tray - if (serverConfig.systemTrayEnabled) { + if (serverConfig.systemTrayEnabled.value) { try { - systemTrayInstance + SystemTray.create() } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error e.printStackTrace() } @@ -139,10 +139,10 @@ open class ApplicationTest { System.setProperty("org.eclipse.jetty.LEVEL", "OFF") // socks proxy settings - if (serverConfig.socksProxyEnabled) { - System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost - System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort - logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") + if (serverConfig.socksProxyEnabled.value) { + System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost.value + System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort.value + logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost.value}:${serverConfig.socksProxyPort.value}") } } diff --git a/server/src/test/resources/server-reference.conf b/server/src/test/resources/server-reference.conf index 01057e9a..9d5dc26f 100644 --- a/server/src/test/resources/server-reference.conf +++ b/server/src/test/resources/server-reference.conf @@ -11,8 +11,10 @@ server.socksProxyPort = "" server.downloadAsCbz = false server.autoDownloadNewChapters = false +# requests +server.maxSourcesInParallel = 10 + # updater -server.maxParallelUpdateRequests = 10 server.excludeUnreadChapters = true server.excludeNotStarted = true server.excludeCompleted = true