Skip to content

Commit

Permalink
A somewhat working auto-update plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
Mnemotechnician committed Apr 5, 2023
1 parent fa595e6 commit f8d4f3c
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 22 deletions.
1 change: 1 addition & 0 deletions minchat-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import java.util.*

plugins {
kotlin("jvm") version "1.8.0"
kotlin("plugin.serialization") version "1.8.0"
}

val jarName = "minchat"
Expand Down
7 changes: 5 additions & 2 deletions minchat-client/src/main/kotlin/io/minchat/client/Minchat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.github.mnemotechnician.mkui.extensions.groups.child
import com.github.mnemotechnician.mkui.extensions.runUi
import io.minchat.client.config.MinchatGithubClient
import io.minchat.client.misc.*
import io.minchat.client.plugin.MinchatPluginHandler
import io.minchat.client.ui.chat.ChatFragment
import io.minchat.common.MINCHAT_VERSION
import io.minchat.rest.MinchatRestClient
Expand All @@ -34,8 +35,10 @@ val MinchatDispatcher = newFixedThreadPoolContext(5, "minchat")
*/
class MinchatMod : Mod(), CoroutineScope {
val rootJob = SupervisorJob()
val exceptionHandler = CoroutineExceptionHandler { _, e ->
Log.err("An exception has occurred in MinChat", e)
val exceptionHandler = CoroutineExceptionHandler { _, e ->
if (e !is CancellationException) {
Log.err("An exception has occurred in MinChat", e)
}
}
override val coroutineContext = rootJob + exceptionHandler + MinchatDispatcher

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import io.minchat.common.BuildVersion
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.*
import kotlinx.serialization.json.Json

/**
Expand All @@ -17,14 +17,20 @@ class MinchatGithubClient {
val httpClient = HttpClient(CIO) {
expectSuccess = true
install(ContentNegotiation) {
json()
json(Json {
ignoreUnknownKeys = true
})
}
}

/** "raw.GithubUserContent.com" */
val rawGithubUrl = "https://raw.githubusercontent.com"
/** "api.github.com" */
val githubApiUrl = "https://api.github.com"
/** The raw GitHub user content url of the official minchat repo. */
val rawMinchatRepoUrl = "$rawGithubUrl/minchat-official/minchat/main"
/** The GitHub api url of the official minchat repo. */
val minchatRepoApiUrl = "$githubApiUrl/repos/minchat-official/minchat"

/** Fetches the latest [BuildVersion] of the MinChat client from GitHub. */
suspend fun getLatestStableVersion() =
Expand Down Expand Up @@ -76,8 +82,35 @@ class MinchatGithubClient {
httpClient.get("$rawMinchatRepoUrl/remote/default-url")
.body<String>()

/**
* Returns a list of [GithubRelease]s associated with the official MinChat repo,
* sorted by date in a descending manner.
*/
suspend fun getReleases(): List<GithubRelease> =
httpClient.get("$minchatRepoApiUrl/releases")
.body<List<GithubRelease>>()

data class ChangelogEntry(
val version: BuildVersion,
val description: String
)

@Serializable
data class GithubRelease(
val url: String,
val id: Int,
val name: String,
@SerialName("tag_name")
val tag: String,
val description: String,
val assets: List<GithubAsset>
)

@Serializable
data class GithubAsset(
val url: String,
@SerialName("browser_download_url")
val downloadUrl: String,
val name: String
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ fun Throwable.userReadable() = when (this) {
is UnresolvedAddressException -> {
"Could not resolve the server. Check your internet connection or make sure you're connecting to a valid MinChat server."
}
is IllegalStateException -> {
// Caused by calls to error()
"Error: ${message}"
}
is RuntimeException -> {
"Mod error: ${toString()}"
"Mod error: $this"
}
else -> "Unknown error: $this. Please, report to the developer."
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package io.minchat.client
package io.minchat.client.plugin

import arc.util.Log
import io.minchat.client.plugin.MinchatPlugin
import io.minchat.client.plugin.impl.NewConsoleIntegrationPlugin
import io.minchat.client.plugin.impl.*
import java.util.concurrent.ConcurrentLinkedQueue

object MinchatPluginHandler {
Expand All @@ -28,7 +27,8 @@ object MinchatPluginHandler {

init {
// default
register(NewConsoleIntegrationPlugin())
// TODO: broken: register(NewConsoleIntegrationPlugin())
register(AutoupdatePlugin())
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package io.minchat.client.plugin.impl

import arc.Core
import arc.graphics.Color
import arc.util.Log
import com.github.mnemotechnician.mkui.extensions.dsl.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.minchat.client.Minchat
import io.minchat.client.misc.userReadable
import io.minchat.client.plugin.MinchatPlugin
import io.minchat.client.ui.ModalDialog
import io.minchat.common.*
import kotlinx.coroutines.*
import mindustry.Vars
import io.minchat.client.misc.MinchatStyle as Style

class AutoupdatePlugin : MinchatPlugin("autoupdater") {
val maxAttempts = 3

override suspend fun onLoad() {
lateinit var latestVersion: BuildVersion
var attempt = 1
while (true) {
try {
latestVersion = Minchat.githubClient.getLatestStableVersion()
break
} catch (e: Exception) {
if (attempt++ == maxAttempts) {
Log.err("Failed to get latest version from GitHub after $maxAttempts attempts.", e)
return
}
}
}

if (latestVersion <= MINCHAT_VERSION) return

UpdatePromptDialog(latestVersion).show()
}

/** Performs an automatic update of the MinChat client. */
fun performUpdate() {
lateinit var job: Job

Vars.ui.loadfrag.apply {
show("Updating. Please wait...")
setButton {
job.cancel()
SimpleInfoDialog("The update has been aborted.").show()
}

job = Minchat.launch {
runCatching {
// Firstly, determine where the mod is located
val file = Vars.mods.getMod(Minchat.javaClass)?.file
?: error("Cannot locate the MinChat mod file.")
if (file.isDirectory) error("MinChat mod file is a directory. This is not supported.")

// Then, fetch the latest version and the list of releases
val latestVersion = Minchat.githubClient.getLatestStableVersion().toString()
val releases = Minchat.githubClient.getReleases()

// Then, try to find the former among the latter.
// If not found, fall back to the latest version.
val latestRelease = releases.find {
(it.tag.removePrefix("v").trim() == latestVersion) && it.assets.any {
it.name.startsWith("minchat-client", true)
&& it.name.endsWith(".jar", true)
}
} ?: releases.firstOrNull()

val modAsset = latestRelease?.assets?.find { it.name.startsWith("minchat-client", true) }
?: error("Cannot locate a suitable mod file. Please, try again later.")

// Download the asset and overwrite the mod file
Minchat.githubClient.httpClient.get(modAsset.downloadUrl) {
onDownload { received, length ->
Vars.ui.loadfrag.setProgress(received.toFloat() / length)
}
}.body<ByteArray>().let { file.writeBytes(it) }

RestartPromptDialog().show()
}.onFailure { exception ->
if (exception is CancellationException) return@onFailure

SimpleInfoDialog("""
Failed to update.
Reason: ${exception.userReadable()}
""".trimIndent()).show()
}
Vars.ui.loadfrag.hide()
}
}
}

inner class UpdatePromptDialog(val latestVersion: BuildVersion) : ModalDialog() {
init {
fields.apply {
addTable(Style.surfaceBackground) {
margin(Style.layoutMargin)
addLabel("""
A new MinChat version is available!
Current version: $MINCHAT_VERSION
Latest version: $latestVersion
""".trimIndent(), Style.Label).pad(Style.layoutPad)
}.fillX().pad(Style.layoutPad).row()

if (!MINCHAT_VERSION.isInterchangeableWith(latestVersion)) addTable(Style.surfaceBackground) {
margin(Style.layoutMargin)

if (!latestVersion.isCompatibleWith(MINCHAT_VERSION)) {
addLabel("The current version is incompatible with the official MinChat server!")
.pad(Style.layoutPad).row()
addLabel("You will likely be unable to chat until you update!")
.color(Color.red).pad(Style.layoutPad)
} else {
addLabel("The current version may not be fully compatible with the official MinChat server.")
.pad(Style.layoutPad)
}
}.fillX().pad(Style.layoutPad).row()

addTable(Style.surfaceBackground) {
margin(Style.layoutMargin)

addLabel("Do you want to update now?").pad(Style.layoutPad)
}.fillX().pad(Style.layoutPad).row()

action("[green]Update") {
hide()
performUpdate()
}
}
}
}

inner class SimpleInfoDialog(val text: String) : ModalDialog() {
init {
fields.addTable(Style.surfaceBackground) {
margin(Style.layoutMargin)

addLabel(text, wrap = true)
.fillX().pad(Style.layoutPad)
.minWidth(300f)
}.fillX()
}
}

inner class RestartPromptDialog : ModalDialog() {
init {
fields.addTable(Style.surfaceBackground) {
val begin = System.currentTimeMillis()

margin(Style.layoutMargin)

addLabel("MinChat has been updated. Restart now?")
.pad(Style.layoutPad).row()
addLabel({ "Restarting automatically in ${
5 - (System.currentTimeMillis() - begin) / 1000
}..." })
.pad(Style.layoutPad)

update {
if (scene != null && System.currentTimeMillis() - begin >= 5) {
Core.app.exit()
}
}
}

action("[green]Restart") {
Core.app.exit()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.minchat.client.ui.chat

import arc.scene.ui.TextButton
import arc.util.Align
import io.minchat.rest.entity.MinchatChannel
import kotlinx.coroutines.CoroutineScope
import io.minchat.client.misc.MinchatStyle as Style

class ChannelButton(
val chat: ChatFragment,
val channel: MinchatChannel
) : TextButton("#${channel.name}", Style.ChannelButton), CoroutineScope by chat {
init {
margin(Style.buttonMargin)

left()
label.setAlignment(Align.left)

clicked {
chat.apply {
currentChannel = channel
updateChatUi()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import arc.scene.Element
import arc.scene.event.Touchable
import arc.scene.ui.*
import arc.scene.ui.layout.*
import arc.util.Align
import com.github.mnemotechnician.mkui.extensions.dsl.*
import com.github.mnemotechnician.mkui.extensions.elements.*
import com.github.mnemotechnician.mkui.extensions.runUi
Expand Down Expand Up @@ -296,16 +295,8 @@ class ChatFragment(parentScope: CoroutineScope) : Fragment<Table, Table>(parentS
channelsContainer.clearChildren()

channels.forEach { channel ->
// TODO: make a separate class
channelsContainer.textButton("#${channel.name}", Style.ChannelButton, align = Align.left) {
currentChannel = channel
lastChatUpdateJob?.cancel()
lastChatUpdateJob = updateChatUi()
}.with {
it.label.setColor(Style.foreground)
}
.pad(Style.layoutPad).margin(Style.buttonMargin)
.align(Align.left).growX().row()
channelsContainer.add(ChannelButton(this, channel))
.pad(Style.layoutPad).growX().row()
}
}

Expand All @@ -318,6 +309,8 @@ class ChatFragment(parentScope: CoroutineScope) : Fragment<Table, Table>(parentS
val channel = currentChannel ?: return null
val notif = notification("Loading messages...", 10)

lastChatUpdateJob?.cancel()

return launch {
val messages = runSafe {
channel.getAllMessages(limit = 50).toList().reversed()
Expand Down Expand Up @@ -378,6 +371,8 @@ class ChatFragment(parentScope: CoroutineScope) : Fragment<Table, Table>(parentS
chatPane.validate()
chatPane.setScrollYForce(chatPane.maxY)
}
}.also {
lastChatUpdateJob = it
}.then { notif.cancel() }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import kotlinx.serialization.Serializable
val MINCHAT_VERSION = BuildVersion(
major = 0,
minor = 3,
patch = 0
patch = 1
)

/**
Expand Down
2 changes: 1 addition & 1 deletion remote/latest-stable.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"major": 0,
"minor": 3,
"patch": 0
"patch": 1
}

0 comments on commit f8d4f3c

Please sign in to comment.