Feature/gql improve webui update status (#783)

* Remove "updateAvailable" from webui update info

Doesn't add anything

* Extract status creation into function

* Optionally emit status immediately

Otherwise, some emissions can get lost due to the 1s sample period

* Rename "STOPPED" state to "IDLE"

* Reset webui update status

Currently, the update status never gets reset.
Thus, it will be "FINISHED" or "ERROR" until the next update gets triggered.
Due to this, the client won't know that the update result was already handled and will handle it again the next time it gets the update status.

To prevent this, the client has to be able to tell the server that it has handled the update result and that it can be resetted
This commit is contained in:
schroda
2023-12-09 01:17:25 +01:00
committed by GitHub
parent df57070b70
commit fb545947ec
3 changed files with 65 additions and 48 deletions

View File

@@ -4,11 +4,9 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
import suwayomi.tachidesk.graphql.types.UpdateState.ERROR import suwayomi.tachidesk.graphql.types.UpdateState.ERROR
import suwayomi.tachidesk.graphql.types.UpdateState.STOPPED import suwayomi.tachidesk.graphql.types.UpdateState.IDLE
import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.util.WebInterfaceManager import suwayomi.tachidesk.server.util.WebInterfaceManager
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -37,16 +35,7 @@ class InfoMutation {
return@withTimeout WebUIUpdatePayload( return@withTimeout WebUIUpdatePayload(
input.clientMutationId, input.clientMutationId,
WebUIUpdateStatus( WebInterfaceManager.getStatus(version, if (didUpdateCheckFail) ERROR else IDLE),
info =
WebUIUpdateInfo(
channel = serverConfig.webUIChannel.value,
tag = version,
updateAvailable,
),
state = if (didUpdateCheckFail) ERROR else STOPPED,
progress = 0,
),
) )
} }
try { try {
@@ -62,4 +51,19 @@ class InfoMutation {
} }
} }
} }
fun resetWebUIUpdateStatus(): CompletableFuture<WebUIUpdateStatus> {
return future {
withTimeout(30.seconds) {
val isUpdateFinished = WebInterfaceManager.status.value.state != DOWNLOADING
if (!isUpdateFinished) {
throw Exception("Status reset is not allowed during status \"$DOWNLOADING\"")
}
WebInterfaceManager.resetStatus()
WebInterfaceManager.status.first { it.state == IDLE }
}
}
}
} }

View File

@@ -14,11 +14,10 @@ data class WebUIUpdateCheck(
data class WebUIUpdateInfo( data class WebUIUpdateInfo(
val channel: String, val channel: String,
val tag: String, val tag: String,
val updateAvailable: Boolean,
) )
enum class UpdateState { enum class UpdateState {
STOPPED, IDLE,
DOWNLOADING, DOWNLOADING,
FINISHED, FINISHED,
ERROR, ERROR,

View File

@@ -41,7 +41,7 @@ import suwayomi.tachidesk.graphql.types.UpdateState
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
import suwayomi.tachidesk.graphql.types.UpdateState.ERROR import suwayomi.tachidesk.graphql.types.UpdateState.ERROR
import suwayomi.tachidesk.graphql.types.UpdateState.FINISHED import suwayomi.tachidesk.graphql.types.UpdateState.FINISHED
import suwayomi.tachidesk.graphql.types.UpdateState.STOPPED import suwayomi.tachidesk.graphql.types.UpdateState.IDLE
import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
@@ -137,25 +137,22 @@ object WebInterfaceManager {
private val notifyFlow = private val notifyFlow =
MutableSharedFlow<WebUIUpdateStatus>(extraBufferCapacity = 1, onBufferOverflow = DROP_OLDEST) MutableSharedFlow<WebUIUpdateStatus>(extraBufferCapacity = 1, onBufferOverflow = DROP_OLDEST)
@OptIn(FlowPreview::class) private val statusFlow = MutableSharedFlow<WebUIUpdateStatus>()
val status = val status =
notifyFlow.sample(1.seconds) statusFlow.stateIn(
.stateIn( scope,
scope, SharingStarted.Eagerly,
SharingStarted.Eagerly, getStatus(),
WebUIUpdateStatus( )
info =
WebUIUpdateInfo(
channel = serverConfig.webUIChannel.value,
tag = "",
updateAvailable = false,
),
state = STOPPED,
progress = 0,
),
)
init { init {
scope.launch {
@OptIn(FlowPreview::class)
notifyFlow.sample(1.seconds).collect {
statusFlow.emit(it)
}
}
serverConfig.subscribeTo( serverConfig.subscribeTo(
combine(serverConfig.webUIUpdateCheckInterval, serverConfig.webUIFlavor) { interval, flavor -> combine(serverConfig.webUIUpdateCheckInterval, serverConfig.webUIFlavor) { interval, flavor ->
Pair( Pair(
@@ -168,7 +165,7 @@ object WebInterfaceManager {
) )
} }
suspend fun getAboutInfo(): AboutWebUI { fun getAboutInfo(): AboutWebUI {
val currentVersion = getLocalVersion() val currentVersion = getLocalVersion()
val failedToGetVersion = currentVersion === "r-1" val failedToGetVersion = currentVersion === "r-1"
@@ -182,6 +179,26 @@ object WebInterfaceManager {
) )
} }
fun getStatus(
version: String = "",
state: UpdateState = IDLE,
progress: Int = 0,
): WebUIUpdateStatus {
return WebUIUpdateStatus(
info =
WebUIUpdateInfo(
channel = serverConfig.webUIChannel.value,
tag = version,
),
state,
progress,
)
}
fun resetStatus() {
emitStatus("", IDLE, 0, immediate = true)
}
private var serveWebUI: () -> Unit = {} private var serveWebUI: () -> Unit = {}
fun setServeWebUI(serveWebUI: () -> Unit) { fun setServeWebUI(serveWebUI: () -> Unit) {
@@ -535,20 +552,17 @@ object WebInterfaceManager {
version: String, version: String,
state: UpdateState, state: UpdateState,
progress: Int, progress: Int,
immediate: Boolean = false,
) { ) {
scope.launch { scope.launch {
notifyFlow.emit( val status = getStatus(version, state, progress)
WebUIUpdateStatus(
info = if (immediate) {
WebUIUpdateInfo( statusFlow.emit(status)
channel = serverConfig.webUIChannel.value, return@launch
tag = version, }
updateAvailable = true,
), notifyFlow.emit(status)
state,
progress,
),
)
} }
} }
@@ -559,7 +573,7 @@ object WebInterfaceManager {
} }
suspend fun downloadVersion(version: String) { suspend fun downloadVersion(version: String) {
emitStatus(version, DOWNLOADING, 0) emitStatus(version, DOWNLOADING, 0, immediate = true)
try { try {
val webUIZip = "${WebUIFlavor.WEBUI.baseFileName}-$version.zip" val webUIZip = "${WebUIFlavor.WEBUI.baseFileName}-$version.zip"
@@ -586,11 +600,11 @@ object WebInterfaceManager {
extractDownload(webUIZipPath, applicationDirs.webUIRoot) extractDownload(webUIZipPath, applicationDirs.webUIRoot)
log.info { "Extracting WebUI zip Done." } log.info { "Extracting WebUI zip Done." }
emitStatus(version, FINISHED, 100) emitStatus(version, FINISHED, 100, immediate = true)
serveWebUI() serveWebUI()
} catch (e: Exception) { } catch (e: Exception) {
emitStatus(version, ERROR, 0) emitStatus(version, ERROR, 0, immediate = true)
throw e throw e
} }
} }