From df5787e5b79fe12cf1bb6aff03382143da62dbab Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 31 Mar 2023 14:03:43 -0700 Subject: [PATCH] Implement optional automatic update mechanism. --- .../core/features/update/UpdateCommand.kt | 11 +++- .../core/features/update/UpdateConfig.kt | 13 ++++ .../core/features/update/UpdateFeature.kt | 34 ++++++++++- .../core/features/update/UpdatePlan.kt | 3 +- .../core/features/update/UpdateResolver.kt | 15 ++++- .../core/features/update/UpdateService.kt | 59 +++++++++++++------ .../core/features/update/UpdateUtil.kt | 18 ------ .../src/main/resources/update.yaml | 3 + 8 files changed, 114 insertions(+), 42 deletions(-) create mode 100644 foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateConfig.kt create mode 100644 foundation-core/src/main/resources/update.yaml diff --git a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateCommand.kt b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateCommand.kt index 125f347..74f0b53 100644 --- a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateCommand.kt +++ b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateCommand.kt @@ -3,15 +3,22 @@ package gay.pizza.foundation.core.features.update import org.bukkit.command.Command import org.bukkit.command.CommandExecutor import org.bukkit.command.CommandSender +import org.bukkit.plugin.Plugin -class UpdateCommand : CommandExecutor { +class UpdateCommand(val plugin: Plugin) : CommandExecutor { override fun onCommand( sender: CommandSender, command: Command, label: String, args: Array ): Boolean { - UpdateService.updatePlugins(sender) + val shouldRestart = args.isNotEmpty() && args[0] == "restart" + UpdateService.updatePlugins(plugin, sender, onFinish = { updated -> + if (!updated) return@updatePlugins + if (shouldRestart) { + sender.server.shutdown() + } + }) return true } } diff --git a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateConfig.kt b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateConfig.kt new file mode 100644 index 0000000..586a5f8 --- /dev/null +++ b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateConfig.kt @@ -0,0 +1,13 @@ +package gay.pizza.foundation.core.features.update + +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateConfig( + val autoUpdateSchedule: AutoUpdateSchedule +) + +@Serializable +class AutoUpdateSchedule( + val cron: String = "" +) diff --git a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateFeature.kt b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateFeature.kt index 493e054..7d7e014 100644 --- a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateFeature.kt +++ b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateFeature.kt @@ -1,9 +1,41 @@ package gay.pizza.foundation.core.features.update import gay.pizza.foundation.core.abstraction.Feature +import gay.pizza.foundation.core.features.scheduler.cancel +import gay.pizza.foundation.core.features.scheduler.cron +import org.koin.core.component.inject +import org.koin.core.module.Module +import org.koin.dsl.module class UpdateFeature : Feature() { + private val config by inject() + lateinit var autoUpdateScheduleId: String + override fun enable() { - plugin.registerCommandExecutor("fupdate", UpdateCommand()) + plugin.registerCommandExecutor("fupdate", UpdateCommand(plugin)) + + if (config.autoUpdateSchedule.cron.isNotEmpty()) { + autoUpdateScheduleId = scheduler.cron(config.autoUpdateSchedule.cron) { + plugin.server.scheduler.runTask(plugin) { -> + plugin.server.dispatchCommand(plugin.server.consoleSender, "fupdate restart") + } + } + } + } + + override fun disable() { + if (::autoUpdateScheduleId.isInitialized) { + scheduler.cancel(autoUpdateScheduleId) + } + } + + override fun module(): Module = module { + single { + plugin.loadConfigurationWithDefault( + plugin, + UpdateConfig.serializer(), + "update.yaml" + ) + } } } diff --git a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdatePlan.kt b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdatePlan.kt index 18afdc9..c7654d8 100644 --- a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdatePlan.kt +++ b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdatePlan.kt @@ -4,5 +4,6 @@ import gay.pizza.foundation.concrete.ExtensibleManifestItem import org.bukkit.plugin.Plugin class UpdatePlan( - val items: Map + val installedSet: Map, + val updateSet: Map ) diff --git a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateResolver.kt b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateResolver.kt index 6fce843..d084cf6 100644 --- a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateResolver.kt +++ b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateResolver.kt @@ -34,7 +34,20 @@ class UpdateResolver { installSet[newDependency] = null } } - return UpdatePlan(installSet) + + val updateSet = installSet.filter { entry -> + if (entry.value == null) { + true + } else { + val installed = entry.value!!.description.version + if (installed == "DEV") { + false + } else { + entry.key.version != installed + } + } + } + return UpdatePlan(installSet, updateSet) } companion object { diff --git a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateService.kt b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateService.kt index c6e5b95..1e67ce7 100644 --- a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateService.kt +++ b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateService.kt @@ -1,11 +1,21 @@ package gay.pizza.foundation.core.features.update import org.bukkit.command.CommandSender +import org.bukkit.plugin.Plugin +import java.util.concurrent.atomic.AtomicBoolean import kotlin.io.path.name import kotlin.io.path.toPath object UpdateService { - fun updatePlugins(sender: CommandSender, onFinish: (() -> Unit)? = null) { + private val running = AtomicBoolean(false) + + fun updatePlugins(plugin: Plugin, sender: CommandSender, onFinish: (Boolean) -> Unit = {}) { + if (!running.compareAndSet(false, true)) { + sender.sendMessage("Update is already running, skipping the requested update.") + onFinish(false) + + return + } val updateDir = sender.server.pluginsFolder.resolve("update") updateDir.mkdir() if (!updateDir.exists()) { @@ -15,26 +25,37 @@ object UpdateService { val updatePath = updateDir.toPath() Thread { - val resolver = UpdateResolver() - val manifest = resolver.fetchCurrentManifest() - val plan = resolver.resolve(manifest, sender.server) - sender.sendMessage("Updates:") - plan.items.forEach { (item, plugin) -> - val pluginJarFileItem = item.files.firstOrNull { it.type == "plugin-jar" } - if (pluginJarFileItem == null) { - sender.sendMessage("WARNING: ${item.name} is required but plugin-jar file not found in manifest. Skipping.") - return@forEach + try { + val resolver = UpdateResolver() + val manifest = resolver.fetchCurrentManifest() + val plan = resolver.resolve(manifest, sender.server) + if (plan.updateSet.isEmpty()) { + onFinish(false) + running.set(false) + return@Thread } - val maybeExistingPluginFileName = plugin?.javaClass?.protectionDomain?.codeSource?.location?.toURI()?.toPath()?.name - val fileName = maybeExistingPluginFileName ?: "${item.name}.jar" - val artifactPath = pluginJarFileItem.path + sender.sendMessage("Updates:") + plan.updateSet.forEach { (item, plugin) -> + val pluginJarFileItem = item.files.firstOrNull { it.type == "plugin-jar" } + if (pluginJarFileItem == null) { + sender.sendMessage("WARNING: ${item.name} is required but plugin-jar file not found in manifest. Skipping.") + return@forEach + } + val maybeExistingPluginFileName = plugin?.javaClass?.protectionDomain?.codeSource?.location?.toURI()?.toPath()?.name + val fileName = maybeExistingPluginFileName ?: "${item.name}.jar" + val artifactPath = pluginJarFileItem.path - sender.sendMessage("${item.name}: Updating ${plugin?.description?.version ?: "[not-installed]"} to ${item.version}") - UpdateUtil.downloadArtifact(artifactPath, updatePath.resolve(fileName)) + sender.sendMessage("${item.name}: Updating ${plugin?.description?.version ?: "[not-installed]"} to ${item.version}") + UpdateUtil.downloadArtifact(artifactPath, updatePath.resolve(fileName)) + } + sender.sendMessage("Restart for updates to take effect.") + onFinish(true) + } catch (e: Exception) { + plugin.slF4JLogger.error("Failed to update Foundation.", e) + onFinish(false) + } finally { + running.set(false) } - sender.sendMessage("Restart for updates to take effect.") - - if (onFinish != null) onFinish() - }.start() + }.apply { name = "Plugin Updater" }.start() } } diff --git a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateUtil.kt b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateUtil.kt index 8b9dc56..d00f0d9 100644 --- a/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateUtil.kt +++ b/foundation-core/src/main/kotlin/gay/pizza/foundation/core/features/update/UpdateUtil.kt @@ -17,24 +17,6 @@ object UpdateUtil { .resolve(path) .toString() - private inline fun fetchFile(url: String, strategy: DeserializationStrategy): T { - val request = HttpRequest - .newBuilder() - .GET() - .uri(URI.create(url)) - .build() - - val response = client.send( - request, - HttpResponse.BodyHandlers.ofString() - ) - - return Json.decodeFromString( - strategy, - response.body() - ) - } - fun downloadArtifact(path: String, outPath: Path) { val uri = URI.create(getUrl(path)) val request = HttpRequest diff --git a/foundation-core/src/main/resources/update.yaml b/foundation-core/src/main/resources/update.yaml new file mode 100644 index 0000000..e3afcc2 --- /dev/null +++ b/foundation-core/src/main/resources/update.yaml @@ -0,0 +1,3 @@ +# Automatic update schedule. +autoUpdateSchedule: + cron: ""