Implement optional automatic update mechanism.

This commit is contained in:
Alex Zenla 2023-03-31 14:03:43 -07:00
parent b7ce799593
commit df5787e5b7
Signed by: alex
GPG Key ID: C0780728420EBFE5
8 changed files with 114 additions and 42 deletions

View File

@ -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<out String>
): 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
}
}

View File

@ -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 = ""
)

View File

@ -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<UpdateConfig>()
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"
)
}
}
}

View File

@ -4,5 +4,6 @@ import gay.pizza.foundation.concrete.ExtensibleManifestItem
import org.bukkit.plugin.Plugin
class UpdatePlan(
val items: Map<ExtensibleManifestItem, Plugin?>
val installedSet: Map<ExtensibleManifestItem, Plugin?>,
val updateSet: Map<ExtensibleManifestItem, Plugin?>
)

View File

@ -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 {

View File

@ -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()
}
}

View File

@ -17,24 +17,6 @@ object UpdateUtil {
.resolve(path)
.toString()
private inline fun <reified T> fetchFile(url: String, strategy: DeserializationStrategy<T>): 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

View File

@ -0,0 +1,3 @@
# Automatic update schedule.
autoUpdateSchedule:
cron: ""