mirror of
https://github.com/GayPizzaSpecifications/foundation.git
synced 2025-08-03 05:30:55 +00:00
Implement optional automatic update mechanism.
This commit is contained in:
@ -3,15 +3,22 @@ package gay.pizza.foundation.core.features.update
|
|||||||
import org.bukkit.command.Command
|
import org.bukkit.command.Command
|
||||||
import org.bukkit.command.CommandExecutor
|
import org.bukkit.command.CommandExecutor
|
||||||
import org.bukkit.command.CommandSender
|
import org.bukkit.command.CommandSender
|
||||||
|
import org.bukkit.plugin.Plugin
|
||||||
|
|
||||||
class UpdateCommand : CommandExecutor {
|
class UpdateCommand(val plugin: Plugin) : CommandExecutor {
|
||||||
override fun onCommand(
|
override fun onCommand(
|
||||||
sender: CommandSender,
|
sender: CommandSender,
|
||||||
command: Command,
|
command: Command,
|
||||||
label: String,
|
label: String,
|
||||||
args: Array<out String>
|
args: Array<out String>
|
||||||
): Boolean {
|
): 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
|
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
|
package gay.pizza.foundation.core.features.update
|
||||||
|
|
||||||
import gay.pizza.foundation.core.abstraction.Feature
|
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() {
|
class UpdateFeature : Feature() {
|
||||||
|
private val config by inject<UpdateConfig>()
|
||||||
|
lateinit var autoUpdateScheduleId: String
|
||||||
|
|
||||||
override fun enable() {
|
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
|
import org.bukkit.plugin.Plugin
|
||||||
|
|
||||||
class UpdatePlan(
|
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
|
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 {
|
companion object {
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
package gay.pizza.foundation.core.features.update
|
package gay.pizza.foundation.core.features.update
|
||||||
|
|
||||||
import org.bukkit.command.CommandSender
|
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.name
|
||||||
import kotlin.io.path.toPath
|
import kotlin.io.path.toPath
|
||||||
|
|
||||||
object UpdateService {
|
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")
|
val updateDir = sender.server.pluginsFolder.resolve("update")
|
||||||
updateDir.mkdir()
|
updateDir.mkdir()
|
||||||
if (!updateDir.exists()) {
|
if (!updateDir.exists()) {
|
||||||
@ -15,26 +25,37 @@ object UpdateService {
|
|||||||
val updatePath = updateDir.toPath()
|
val updatePath = updateDir.toPath()
|
||||||
|
|
||||||
Thread {
|
Thread {
|
||||||
val resolver = UpdateResolver()
|
try {
|
||||||
val manifest = resolver.fetchCurrentManifest()
|
val resolver = UpdateResolver()
|
||||||
val plan = resolver.resolve(manifest, sender.server)
|
val manifest = resolver.fetchCurrentManifest()
|
||||||
sender.sendMessage("Updates:")
|
val plan = resolver.resolve(manifest, sender.server)
|
||||||
plan.items.forEach { (item, plugin) ->
|
if (plan.updateSet.isEmpty()) {
|
||||||
val pluginJarFileItem = item.files.firstOrNull { it.type == "plugin-jar" }
|
onFinish(false)
|
||||||
if (pluginJarFileItem == null) {
|
running.set(false)
|
||||||
sender.sendMessage("WARNING: ${item.name} is required but plugin-jar file not found in manifest. Skipping.")
|
return@Thread
|
||||||
return@forEach
|
|
||||||
}
|
}
|
||||||
val maybeExistingPluginFileName = plugin?.javaClass?.protectionDomain?.codeSource?.location?.toURI()?.toPath()?.name
|
sender.sendMessage("Updates:")
|
||||||
val fileName = maybeExistingPluginFileName ?: "${item.name}.jar"
|
plan.updateSet.forEach { (item, plugin) ->
|
||||||
val artifactPath = pluginJarFileItem.path
|
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}")
|
sender.sendMessage("${item.name}: Updating ${plugin?.description?.version ?: "[not-installed]"} to ${item.version}")
|
||||||
UpdateUtil.downloadArtifact(artifactPath, updatePath.resolve(fileName))
|
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.")
|
}.apply { name = "Plugin Updater" }.start()
|
||||||
|
|
||||||
if (onFinish != null) onFinish()
|
|
||||||
}.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,24 +17,6 @@ object UpdateUtil {
|
|||||||
.resolve(path)
|
.resolve(path)
|
||||||
.toString()
|
.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) {
|
fun downloadArtifact(path: String, outPath: Path) {
|
||||||
val uri = URI.create(getUrl(path))
|
val uri = URI.create(getUrl(path))
|
||||||
val request = HttpRequest
|
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: ""
|
Reference in New Issue
Block a user