mirror of
https://github.com/GayPizzaSpecifications/foundation.git
synced 2025-08-02 21:20:55 +00:00
Implement optional automatic update mechanism.
This commit is contained in:
parent
b7ce799593
commit
df5787e5b7
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 = ""
|
||||
)
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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?>
|
||||
)
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
3
foundation-core/src/main/resources/update.yaml
Normal file
3
foundation-core/src/main/resources/update.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
# Automatic update schedule.
|
||||
autoUpdateSchedule:
|
||||
cron: ""
|
Loading…
Reference in New Issue
Block a user