mirror of
https://gitlab.com/mangadex-pub/mangadex_at_home.git
synced 2024-01-19 02:48:37 +00:00
ClientSettings will be immediately copied over for live use.
If the Server does not need to be restarted, but the WebUI does, the WebUI will be stopped, the ClientSettings will be copied over, then the WebUI will be started back again (if the new settings still require a WebUI). If the Server and WebUI need to be restarted, the WebUI will be stopped and a Graceful Shutdown will be started follow by a transition to the 'ReloadClientSettings' state. Once in that state the ClientSettings will be copied over then both the Server and WebUI (if the new settings still require a WebUI) will be started back again. If the WebUI does not need to be restarted but the Server does, a Graceful Shutdown will be started followed by a transition to the 'ReloadClientSettings' state. Once in that state, the ClientSettings will be copied over then the Server will be started back
This commit is contained in:
parent
e2897ec595
commit
0e47dfc7f5
|
@ -35,6 +35,8 @@ import mdnet.BuildInfo
|
|||
import mdnet.base.settings.ClientSettings
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class ClientSettingsException(message: String): Exception(message)
|
||||
|
||||
object Main {
|
||||
private val LOGGER = LoggerFactory.getLogger(Main::class.java)
|
||||
private val JACKSON: ObjectMapper = jacksonObjectMapper().enable(SerializationFeature.INDENT_OUTPUT).configure(JsonParser.Feature.ALLOW_COMMENTS, true)
|
||||
|
@ -67,9 +69,8 @@ object Main {
|
|||
} else if (args.isNotEmpty()) {
|
||||
dieWithError("Expected one argument: path to config file, or nothing")
|
||||
}
|
||||
|
||||
val settings = try {
|
||||
JACKSON.readValue<ClientSettings>(FileReader(file))
|
||||
JACKSON.readValue<ClientSettings>(FileReader(file)).apply(::validateSettings)
|
||||
} catch (e: UnrecognizedPropertyException) {
|
||||
dieWithError("'${e.propertyName}' is not a valid setting")
|
||||
} catch (e: JsonProcessingException) {
|
||||
|
@ -83,10 +84,12 @@ object Main {
|
|||
dieWithError(e)
|
||||
}
|
||||
}
|
||||
}.apply(::validateSettings)
|
||||
} catch(e: ClientSettingsException) {
|
||||
dieWithError(e)
|
||||
}
|
||||
|
||||
LOGGER.info { "Client settings loaded: $settings" }
|
||||
val client = MangaDexClient(settings)
|
||||
val client = MangaDexClient(settings, file)
|
||||
Runtime.getRuntime().addShutdownHook(Thread { client.shutdown() })
|
||||
client.runLoop()
|
||||
}
|
||||
|
@ -104,29 +107,29 @@ object Main {
|
|||
exitProcess(1)
|
||||
}
|
||||
|
||||
private fun validateSettings(settings: ClientSettings) {
|
||||
if (!isSecretValid(settings.clientSecret)) dieWithError("Config Error: API Secret is invalid, must be 52 alphanumeric characters")
|
||||
fun validateSettings(settings: ClientSettings) {
|
||||
if (!isSecretValid(settings.clientSecret)) throw ClientSettingsException("Config Error: API Secret is invalid, must be 52 alphanumeric characters")
|
||||
if (settings.clientPort == 0) {
|
||||
dieWithError("Config Error: Invalid port number")
|
||||
throw ClientSettingsException("Config Error: Invalid port number")
|
||||
}
|
||||
if (settings.maxCacheSizeInMebibytes < 1024) {
|
||||
dieWithError("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)")
|
||||
throw ClientSettingsException("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)")
|
||||
}
|
||||
if (settings.threads < 4) {
|
||||
dieWithError("Config Error: Invalid number of threads, must be >= 4")
|
||||
throw ClientSettingsException("Config Error: Invalid number of threads, must be >= 4")
|
||||
}
|
||||
if (settings.maxMebibytesPerHour < 0) {
|
||||
dieWithError("Config Error: Max bandwidth must be >= 0")
|
||||
}
|
||||
if (settings.maxKilobitsPerSecond < 0) {
|
||||
dieWithError("Config Error: Max burst rate must be >= 0")
|
||||
throw ClientSettingsException("Config Error: Max burst rate must be >= 0")
|
||||
}
|
||||
if (settings.gracefulShutdownWaitSeconds < 15) {
|
||||
dieWithError("Config Error: Graceful shutdown wait be >= 15")
|
||||
throw ClientSettingsException("Config Error: Graceful shutdown wait be >= 15")
|
||||
}
|
||||
if (settings.webSettings != null) {
|
||||
if (settings.webSettings.uiPort == 0) {
|
||||
dieWithError("Config Error: Invalid UI port number")
|
||||
throw ClientSettingsException("Config Error: Invalid UI port number")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,12 @@ along with this MangaDex@Home. If not, see <http://www.gnu.org/licenses/>.
|
|||
package mdnet.base
|
||||
|
||||
import ch.qos.logback.classic.LoggerContext
|
||||
import com.fasterxml.jackson.core.JsonProcessingException
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import mdnet.base.Main.dieWithError
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.time.Instant
|
||||
|
@ -32,7 +35,6 @@ import java.util.concurrent.Executors
|
|||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import mdnet.base.Main.dieWithError
|
||||
import mdnet.base.data.Statistics
|
||||
import mdnet.base.server.getServer
|
||||
import mdnet.base.server.getUiServer
|
||||
|
@ -42,6 +44,8 @@ import mdnet.cache.DiskLruCache
|
|||
import mdnet.cache.HeaderMismatchException
|
||||
import org.http4k.server.Http4kServer
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.FileReader
|
||||
import java.io.FileWriter
|
||||
|
||||
sealed class State
|
||||
// server is not running
|
||||
|
@ -52,8 +56,10 @@ object Shutdown : State()
|
|||
data class GracefulShutdown(val lastRunning: Running, val counts: Int = 0, val nextState: State = Uninitialized, val action: () -> Unit = {}) : State()
|
||||
// server is currently running
|
||||
data class Running(val server: Http4kServer, val settings: ServerSettings, val minutesToSettingsReload: Int) : State()
|
||||
|
||||
class MangaDexClient(private val clientSettings: ClientSettings) {
|
||||
// server is stopped to reload client settings
|
||||
data class ReloadClientSettings(val newSettings: ClientSettings, val restartWebUI: Boolean = false): State()
|
||||
// clientSettings must only be accessed from the thread on the executorService
|
||||
class MangaDexClient(private var clientSettings: ClientSettings, private val clientSettingsFile: String) {
|
||||
// this must remain singlethreaded because of how the state mechanism works
|
||||
private val executorService = Executors.newSingleThreadScheduledExecutor()
|
||||
// state must only be accessed from the thread on the executorService
|
||||
|
@ -73,11 +79,12 @@ class MangaDexClient(private val clientSettings: ClientSettings) {
|
|||
private var webUi: Http4kServer? = null
|
||||
private val cache: DiskLruCache
|
||||
|
||||
|
||||
init {
|
||||
try {
|
||||
cache = DiskLruCache.open(
|
||||
File("cache"), 1, 1,
|
||||
clientSettings.maxCacheSizeInMebibytes * 1024 * 1024 /* MiB to bytes */
|
||||
File("cache"), 1, 1,
|
||||
clientSettings.maxCacheSizeInMebibytes * 1024 * 1024 /* MiB to bytes */
|
||||
)
|
||||
cache.get("statistics")?.use {
|
||||
statistics.set(JACKSON.readValue<Statistics>(it.getInputStream(0)))
|
||||
|
@ -96,14 +103,14 @@ class MangaDexClient(private val clientSettings: ClientSettings) {
|
|||
statsMap[Instant.now()] = statistics.get()
|
||||
|
||||
if (clientSettings.webSettings != null) {
|
||||
webUi = getUiServer(clientSettings.webSettings, statistics, statsMap)
|
||||
webUi = getUiServer(clientSettings.webSettings!!, statistics, statsMap)
|
||||
webUi!!.start()
|
||||
}
|
||||
LOGGER.info { "Mangadex@Home Client initialized. Starting normal operation." }
|
||||
|
||||
executorService.scheduleAtFixedRate({
|
||||
try {
|
||||
if (state is Running || state is GracefulShutdown || state is Uninitialized) {
|
||||
if (state is Running || state is GracefulShutdown || state is Uninitialized || state is ReloadClientSettings) {
|
||||
statistics.updateAndGet {
|
||||
it.copy(bytesOnDisk = cache.size())
|
||||
}
|
||||
|
@ -201,18 +208,34 @@ class MangaDexClient(private val clientSettings: ClientSettings) {
|
|||
try {
|
||||
val state = this.state
|
||||
if (state is Running) {
|
||||
val minutesToNextReload = state.minutesToSettingsReload
|
||||
val minutesToNextReload = state.minutesToSettingsReload - 1
|
||||
if (minutesToNextReload <= 0) {
|
||||
reloadClientSettings()
|
||||
this.state = state.copy(minutesToSettingsReload = clientSettings.settingsReloadDelayInMinutes)
|
||||
}
|
||||
else {
|
||||
this.state = state.copy(minutesToSettingsReload = minutesToNextReload - 1)
|
||||
this.state = state.copy(minutesToSettingsReload = minutesToNextReload)
|
||||
}
|
||||
}
|
||||
else if (state is ReloadClientSettings) {
|
||||
clientSettings = state.newSettings
|
||||
LOGGER.info("Reloaded ClientSettings: {}", clientSettings)
|
||||
|
||||
LOGGER.info("Starting Server after reloading ClientSettings")
|
||||
this.state = Uninitialized
|
||||
loginAndStartServer()
|
||||
|
||||
// Start the WebUI if we had to stop it
|
||||
// and still want it
|
||||
if(state.restartWebUI && clientSettings.webSettings != null) {
|
||||
LOGGER.info("Starting WebUI after reloading ClientSettings")
|
||||
if (webUi == null) {
|
||||
webUi = getUiServer(clientSettings.webSettings!!, statistics, statsMap)
|
||||
}
|
||||
webUi!!.start()
|
||||
}
|
||||
LOGGER.info { "Time to next client settings reload: " + minutesToNextReload }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LOGGER.warn(e) { "Client settings reloader failed" }
|
||||
LOGGER.warn(e) { "Reload of ClientSettings failed" }
|
||||
}
|
||||
}, 1, 1, TimeUnit.MINUTES)
|
||||
}
|
||||
|
@ -321,8 +344,76 @@ class MangaDexClient(private val clientSettings: ClientSettings) {
|
|||
* Web UI and/or the server if needed
|
||||
*/
|
||||
private fun reloadClientSettings() {
|
||||
val state = this.state
|
||||
val state = this.state as Running
|
||||
LOGGER.info { "Reloading client settings" }
|
||||
try {
|
||||
val newSettings = JACKSON.readValue<ClientSettings>(FileReader(clientSettingsFile)).apply(Main::validateSettings)
|
||||
|
||||
if(newSettings == clientSettings) {
|
||||
LOGGER.info { "Client Settings have not changed" }
|
||||
return
|
||||
}
|
||||
|
||||
// Setting loaded without issue. Figure out
|
||||
// if there are changes that require a restart
|
||||
val restartServer = newSettings.clientSecret != clientSettings.clientSecret ||
|
||||
newSettings.clientHostname != clientSettings.clientHostname ||
|
||||
newSettings.clientPort != clientSettings.clientPort ||
|
||||
newSettings.clientExternalPort != clientSettings.clientExternalPort ||
|
||||
newSettings.devSettings?.isDev != clientSettings.devSettings?.isDev
|
||||
var restartWebUI = newSettings.webSettings != clientSettings.webSettings ||
|
||||
newSettings.webSettings?.uiHostname != clientSettings.webSettings?.uiHostname ||
|
||||
newSettings.webSettings?.uiPort != clientSettings.webSettings?.uiPort
|
||||
|
||||
// Stop the the WebUI if needed
|
||||
if(restartWebUI && newSettings.webSettings != null) {
|
||||
LOGGER.info("Stopping WebUI to reload ClientSettings")
|
||||
webUi?.stop()
|
||||
} else if(restartWebUI && newSettings.webSettings == null) {
|
||||
LOGGER.info("Stopping WebUI to because it is no longer in ClientSettings")
|
||||
webUi?.close()
|
||||
webUi = null
|
||||
// We don't want it so don't restart it
|
||||
restartWebUI = false
|
||||
}
|
||||
|
||||
if(restartServer) {
|
||||
// If we are restarting the server
|
||||
// We must do it gracefully and set
|
||||
// the new settings later
|
||||
LOGGER.info("Stopping Server to reload ClientSettings")
|
||||
this.state = GracefulShutdown(state, nextState = ReloadClientSettings(newSettings, restartWebUI))
|
||||
} else {
|
||||
// If we aren't restarting the server
|
||||
// We can update the settings now
|
||||
clientSettings = newSettings
|
||||
LOGGER.info("Reloaded ClientSettings: {}", clientSettings)
|
||||
}
|
||||
// Start the WebUI if we had to stop it
|
||||
// and still want it
|
||||
if(restartWebUI && clientSettings.webSettings != null) {
|
||||
LOGGER.info("Starting WebUI after reloading ClientSettings")
|
||||
if (webUi == null) {
|
||||
webUi = getUiServer(clientSettings.webSettings!!, statistics, statsMap)
|
||||
}
|
||||
webUi!!.start()
|
||||
}
|
||||
} catch (e: UnrecognizedPropertyException) {
|
||||
dieWithError("'${e.propertyName}' is not a valid setting")
|
||||
} catch (e: JsonProcessingException) {
|
||||
dieWithError(e)
|
||||
} catch (ignored: IOException) {
|
||||
ClientSettings().also {
|
||||
LOGGER.warn("Settings file {} not found, generating file", clientSettingsFile)
|
||||
try {
|
||||
FileWriter(clientSettingsFile).use { writer -> JACKSON.writeValue(writer, it) }
|
||||
} catch (e: IOException) {
|
||||
dieWithError(e)
|
||||
}
|
||||
}
|
||||
} catch(e: ClientSettingsException) {
|
||||
LOGGER.warn("Can't reload client settings: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
Loading…
Reference in a new issue