1
0
Fork 1
mirror of https://gitlab.com/mangadex-pub/mangadex_at_home.git synced 2024-01-19 02:48:37 +00:00
mangadex_at_home/src/main/kotlin/mdnet/MangaDexClient.kt

233 lines
8.2 KiB
Kotlin
Raw Normal View History

2020-06-22 17:02:36 +00:00
/*
Mangadex@Home
Copyright (c) 2020, MangaDex Network
This file is part of MangaDex@Home.
MangaDex@Home is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
MangaDex@Home is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this MangaDex@Home. If not, see <http://www.gnu.org/licenses/>.
2021-01-25 02:25:49 +00:00
*/
2021-01-24 04:55:11 +00:00
package mdnet
2020-06-21 19:49:10 +00:00
import com.fasterxml.jackson.core.JsonProcessingException
2020-07-02 21:24:12 +00:00
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
2021-01-24 04:55:11 +00:00
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.KotlinModule
2020-06-21 19:49:10 +00:00
import com.fasterxml.jackson.module.kotlin.readValue
2021-01-24 04:55:11 +00:00
import mdnet.Main.dieWithError
import mdnet.cache.ImageStorage
import mdnet.logging.info
import mdnet.logging.warn
import mdnet.settings.ClientSettings
import org.ktorm.database.Database
import org.slf4j.LoggerFactory
2020-07-02 16:06:32 +00:00
import java.io.File
import java.io.FileReader
2020-07-02 16:06:32 +00:00
import java.io.IOException
2021-01-24 04:55:11 +00:00
import java.nio.file.Path
2020-08-11 19:42:00 +00:00
import java.util.concurrent.CountDownLatch
2020-07-02 16:06:32 +00:00
import java.util.concurrent.Executors
2020-08-11 19:42:00 +00:00
import java.util.concurrent.ScheduledFuture
2020-07-02 16:06:32 +00:00
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
2020-06-21 19:49:10 +00:00
// Exception class to handle when Client Settings have invalid values
class ClientSettingsException(message: String) : Exception(message)
2021-01-24 04:55:11 +00:00
class MangaDexClient(private val settingsFile: File, databaseFile: File, cacheFolder: Path) {
2020-08-11 19:35:34 +00:00
// this must remain single-threaded because of how the state mechanism works
private val executor = Executors.newSingleThreadScheduledExecutor()
2021-01-24 04:55:11 +00:00
private var scheduledFuture: ScheduledFuture<*>? = null
2020-08-11 19:31:47 +00:00
2020-08-11 19:12:01 +00:00
private val database: Database
2021-01-24 04:55:11 +00:00
private val storage: ImageStorage
private var settings: ClientSettings
2020-06-21 19:49:10 +00:00
2020-08-11 19:35:34 +00:00
// state that must only be accessed from the thread on the executor
private var imageServer: ServerManager? = null
2020-08-11 19:35:34 +00:00
// end protected state
2020-06-21 19:49:10 +00:00
init {
settings = try {
readClientSettings()
} catch (e: UnrecognizedPropertyException) {
dieWithError("'${e.propertyName}' is not a valid setting")
} catch (e: JsonProcessingException) {
dieWithError(e)
} catch (e: ClientSettingsException) {
dieWithError(e)
} catch (e: IOException) {
dieWithError(e)
}
2020-08-11 19:12:01 +00:00
LOGGER.info { "Client settings loaded: $settings" }
2021-01-24 04:55:11 +00:00
database = Database.connect("jdbc:h2:$databaseFile", "org.h2.Driver")
storage = ImageStorage(
maxSize = (settings.maxCacheSizeInMebibytes * 1024 * 1024 * 0.95).toLong(), /* MiB to bytes */
cacheFolder,
database
)
2020-06-21 19:49:10 +00:00
}
fun runLoop() {
2021-01-24 04:55:11 +00:00
LOGGER.info { "Mangadex@Home Client initialized - starting normal operation" }
2020-08-11 19:12:01 +00:00
startImageServer()
2020-07-04 19:39:11 +00:00
2021-01-24 04:55:11 +00:00
scheduledFuture = executor.scheduleWithFixedDelay(
{
try {
// this blocks the executor, so no worries about concurrency
reloadClientSettings()
} catch (e: Exception) {
LOGGER.warn(e) { "Reload of ClientSettings failed" }
}
},
1, 1, TimeUnit.MINUTES
)
2020-06-21 19:49:10 +00:00
}
// Precondition: settings must be filled with up-to-date settings
private fun startImageServer() {
2020-08-11 19:31:47 +00:00
if (imageServer != null) {
throw AssertionError()
}
2020-08-11 19:12:01 +00:00
LOGGER.info { "Server manager starting" }
2021-01-24 04:55:11 +00:00
imageServer = ServerManager(
settings.serverSettings,
settings.devSettings,
settings.maxCacheSizeInMebibytes,
settings.metricsSettings,
storage
).also {
it.start()
2020-06-21 19:49:10 +00:00
}
2020-08-11 19:12:01 +00:00
LOGGER.info { "Server manager started" }
2020-06-21 19:49:10 +00:00
}
private fun stopImageServer() {
2020-08-11 19:12:01 +00:00
LOGGER.info { "Server manager stopping" }
requireNotNull(imageServer).shutdown()
2020-08-11 19:12:01 +00:00
imageServer = null
LOGGER.info { "Server manager stopped" }
2020-06-21 19:49:10 +00:00
}
fun shutdown() {
2020-08-11 19:42:00 +00:00
LOGGER.info { "Mangadex@Home Client shutting down" }
val latch = CountDownLatch(1)
2021-01-24 04:55:11 +00:00
scheduledFuture?.cancel(false)
2020-08-11 19:42:00 +00:00
2021-01-24 04:55:11 +00:00
executor.schedule(
{
if (imageServer != null) {
stopImageServer()
}
2020-08-11 19:42:00 +00:00
2021-01-24 04:55:11 +00:00
storage.close()
latch.countDown()
},
0, TimeUnit.SECONDS
)
2020-08-11 19:42:00 +00:00
latch.await()
executor.shutdown()
2021-01-24 04:55:11 +00:00
executor.awaitTermination(10, TimeUnit.SECONDS)
2020-08-11 19:42:00 +00:00
LOGGER.info { "Mangadex@Home Client has shut down" }
2020-06-21 19:49:10 +00:00
}
/**
* Reloads the client configuration and restarts the
* Web UI and/or the server if needed
*/
private fun reloadClientSettings() {
LOGGER.info { "Checking client settings" }
try {
val newSettings = readClientSettings()
if (newSettings == settings) {
LOGGER.info { "Client settings unchanged" }
return
}
2020-08-11 19:12:01 +00:00
LOGGER.info { "New settings loaded: $newSettings" }
2021-01-24 04:55:11 +00:00
storage.maxSize = (newSettings.maxCacheSizeInMebibytes * 1024 * 1024 * 0.95).toLong()
val restartServer = newSettings.serverSettings != settings.serverSettings ||
2021-01-24 04:55:11 +00:00
newSettings.devSettings != settings.devSettings ||
newSettings.metricsSettings != settings.metricsSettings
if (restartServer) {
stopImageServer()
startImageServer()
}
2020-08-11 19:54:53 +00:00
settings = newSettings
} catch (e: UnrecognizedPropertyException) {
LOGGER.warn { "Settings file is invalid: '$e.propertyName' is not a valid setting" }
} catch (e: JsonProcessingException) {
LOGGER.warn { "Settings file is invalid: $e.message" }
} catch (e: ClientSettingsException) {
LOGGER.warn { "Settings file is invalid: $e.message" }
} catch (e: IOException) {
LOGGER.warn { "Error loading settings file: $e.message" }
}
}
private fun validateSettings(settings: ClientSettings) {
2021-01-24 18:05:05 +00:00
if (settings.maxCacheSizeInMebibytes < 1024) {
throw ClientSettingsException("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)")
}
fun isSecretValid(clientSecret: String): Boolean {
return Pattern.matches("^[a-zA-Z0-9]{$CLIENT_KEY_LENGTH}$", clientSecret)
}
settings.serverSettings.let {
2021-01-24 04:55:11 +00:00
if (!isSecretValid(it.secret)) {
throw ClientSettingsException("Config Error: API Secret is invalid, must be 52 alphanumeric characters")
}
2021-01-24 04:55:11 +00:00
if (it.port == 0) {
throw ClientSettingsException("Config Error: Invalid port number")
}
2021-01-24 04:55:11 +00:00
if (it.port in Constants.RESTRICTED_PORTS) {
throw ClientSettingsException("Config Error: Unsafe port number")
}
if (it.threads < 4) {
throw ClientSettingsException("Config Error: Invalid number of threads, must be >= 4")
}
if (it.maxMebibytesPerHour < 0) {
throw ClientSettingsException("Config Error: Max bandwidth must be >= 0")
}
if (it.maxKilobitsPerSecond < 0) {
throw ClientSettingsException("Config Error: Max burst rate must be >= 0")
}
if (it.gracefulShutdownWaitSeconds < 15) {
throw ClientSettingsException("Config Error: Graceful shutdown wait must be >= 15")
}
}
}
private fun readClientSettings(): ClientSettings {
2020-08-11 19:12:01 +00:00
return JACKSON.readValue<ClientSettings>(FileReader(settingsFile)).apply(::validateSettings)
}
2020-06-21 19:49:10 +00:00
companion object {
private const val CLIENT_KEY_LENGTH = 52
2020-06-21 19:49:10 +00:00
private val LOGGER = LoggerFactory.getLogger(MangaDexClient::class.java)
2021-01-24 04:55:11 +00:00
private val JACKSON: ObjectMapper = ObjectMapper(YAMLFactory()).registerModule(KotlinModule())
2020-06-21 19:49:10 +00:00
}
}