mirror of
https://gitlab.com/mangadex-pub/mangadex_at_home.git
synced 2024-01-19 02:48:37 +00:00
241 lines
8.4 KiB
Kotlin
241 lines
8.4 KiB
Kotlin
/*
|
|
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/>.
|
|
*/
|
|
package mdnet
|
|
|
|
import com.fasterxml.jackson.core.JsonProcessingException
|
|
import com.fasterxml.jackson.databind.ObjectMapper
|
|
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
|
|
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
|
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
|
import com.fasterxml.jackson.module.kotlin.readValue
|
|
import com.zaxxer.hikari.HikariConfig
|
|
import com.zaxxer.hikari.HikariDataSource
|
|
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
|
|
import java.io.File
|
|
import java.io.FileReader
|
|
import java.io.IOException
|
|
import java.nio.file.Path
|
|
import java.util.concurrent.CountDownLatch
|
|
import java.util.concurrent.Executors
|
|
import java.util.concurrent.ScheduledFuture
|
|
import java.util.concurrent.TimeUnit
|
|
import java.util.regex.Pattern
|
|
|
|
// Exception class to handle when Client Settings have invalid values
|
|
class ClientSettingsException(message: String) : Exception(message)
|
|
|
|
class MangaDexClient(private val settingsFile: File, databaseFolder: Path, cacheFolder: Path) {
|
|
// this must remain single-threaded because of how the state mechanism works
|
|
private val executor = Executors.newSingleThreadScheduledExecutor()
|
|
private var scheduledFuture: ScheduledFuture<*>? = null
|
|
|
|
private val database: Database
|
|
private val storage: ImageStorage
|
|
private val dataSource: HikariDataSource
|
|
|
|
private var settings: ClientSettings
|
|
|
|
// state that must only be accessed from the thread on the executor
|
|
private var imageServer: ServerManager? = null
|
|
// end protected state
|
|
|
|
init {
|
|
settings = readClientSettings()
|
|
|
|
LOGGER.info { "Client settings loaded: $settings" }
|
|
|
|
Class.forName("org.sqlite.JDBC")
|
|
|
|
val config = HikariConfig()
|
|
val db = databaseFolder.resolve("metadata.db")
|
|
config.jdbcUrl = "jdbc:sqlite:$db"
|
|
config.addDataSourceProperty("cachePrepStmts", "true")
|
|
config.addDataSourceProperty("prepStmtCacheSize", "100")
|
|
config.addDataSourceProperty("prepStmtCacheSqlLimit", "1000")
|
|
dataSource = HikariDataSource(config)
|
|
|
|
database = Database.connect(dataSource)
|
|
storage = ImageStorage(
|
|
maxSize = (settings.maxCacheSizeInMebibytes * 1024 * 1024 * 0.95).toLong(), /* MiB to bytes */
|
|
cacheFolder,
|
|
database
|
|
)
|
|
}
|
|
|
|
fun runLoop() {
|
|
LOGGER.info { "Mangadex@Home Client initialized - starting normal operation" }
|
|
|
|
startImageServer()
|
|
|
|
scheduledFuture = executor.scheduleWithFixedDelay(
|
|
{
|
|
try {
|
|
reloadClientSettings()
|
|
} catch (e: Exception) {
|
|
LOGGER.warn(e) { "Reload of ClientSettings failed" }
|
|
}
|
|
},
|
|
1, 1, TimeUnit.MINUTES
|
|
)
|
|
|
|
scheduledFuture = executor.scheduleWithFixedDelay(
|
|
{
|
|
try {
|
|
if (imageServer == null) {
|
|
LOGGER.info { "Restarting image server that failed to start" }
|
|
startImageServer()
|
|
}
|
|
} catch (e: Exception) {
|
|
LOGGER.warn(e) { "Image server restart failed" }
|
|
}
|
|
},
|
|
1, 1, TimeUnit.MINUTES
|
|
)
|
|
}
|
|
|
|
// Precondition: settings must be filled with up-to-date settings
|
|
private fun startImageServer() {
|
|
require(imageServer == null) { "imageServer was not null" }
|
|
LOGGER.info { "Server manager starting" }
|
|
imageServer = ServerManager(
|
|
settings,
|
|
storage
|
|
).also {
|
|
it.start()
|
|
}
|
|
LOGGER.info { "Server manager started" }
|
|
}
|
|
|
|
private fun stopImageServer() {
|
|
LOGGER.info { "Server manager stopping" }
|
|
requireNotNull(imageServer).shutdown()
|
|
imageServer = null
|
|
LOGGER.info { "Server manager stopped" }
|
|
}
|
|
|
|
fun shutdown() {
|
|
LOGGER.info { "Mangadex@Home Client shutting down" }
|
|
val latch = CountDownLatch(1)
|
|
|
|
scheduledFuture?.cancel(false)
|
|
|
|
executor.schedule(
|
|
{
|
|
if (imageServer != null) {
|
|
stopImageServer()
|
|
}
|
|
|
|
storage.close()
|
|
dataSource.close()
|
|
latch.countDown()
|
|
},
|
|
0, TimeUnit.SECONDS
|
|
)
|
|
|
|
latch.await()
|
|
executor.shutdown()
|
|
executor.awaitTermination(10, TimeUnit.SECONDS)
|
|
|
|
LOGGER.info { "Mangadex@Home Client has shut down" }
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
settings = newSettings
|
|
LOGGER.info { "New settings loaded: $newSettings" }
|
|
|
|
storage.maxSize = (newSettings.maxCacheSizeInMebibytes * 1024 * 1024 * 0.95).toLong()
|
|
|
|
if (imageServer != null) {
|
|
stopImageServer()
|
|
}
|
|
try {
|
|
startImageServer()
|
|
} catch (e: Exception) {
|
|
LOGGER.warn(e) { "Error starting the image server" }
|
|
}
|
|
} 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) {
|
|
if (settings.maxCacheSizeInMebibytes < 20480) {
|
|
throw ClientSettingsException("Config Error: Invalid max cache size, must be >= 20480 MiB (20 GiB)")
|
|
}
|
|
|
|
fun isSecretValid(clientSecret: String): Boolean {
|
|
return Pattern.matches("^[a-zA-Z0-9]{$CLIENT_KEY_LENGTH}$", clientSecret)
|
|
}
|
|
|
|
settings.serverSettings.let {
|
|
if (!isSecretValid(it.secret)) {
|
|
throw ClientSettingsException("Config Error: API Secret is invalid, must be 52 alphanumeric characters")
|
|
}
|
|
if (it.port == 0) {
|
|
throw ClientSettingsException("Config Error: Invalid port number")
|
|
}
|
|
if (it.port in Constants.RESTRICTED_PORTS) {
|
|
throw ClientSettingsException("Config Error: Unsafe port number")
|
|
}
|
|
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 {
|
|
return JACKSON.readValue<ClientSettings>(FileReader(settingsFile)).apply(::validateSettings)
|
|
}
|
|
|
|
companion object {
|
|
private const val CLIENT_KEY_LENGTH = 52
|
|
private val LOGGER = LoggerFactory.getLogger(MangaDexClient::class.java)
|
|
private val JACKSON: ObjectMapper = ObjectMapper(YAMLFactory()).registerModule(KotlinModule())
|
|
}
|
|
}
|