diff --git a/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt b/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt index 5123d4d..f59c8f4 100644 --- a/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt +++ b/app/src/main/kotlin/com/shonar/desktop/DesktopState.kt @@ -224,6 +224,42 @@ class DesktopState(private val appDir: File = defaultAppDir()) { } } + /** True when scripts/update-app.sh exists next to the repo the engine + * was spawned from (dev-tree installs only; the update pulls from + * forgejo, rebuilds the distributable, and relaunches). */ + val updateScript: File? + get() = repoRoot()?.let { File(it, "scripts/update-app.sh") } + ?.takeIf { it.canExecute() } + + /** Hand off to the detached updater (git pull + rebuild + relaunch). + * The script runs via setsid so it outlives this process when the + * updater kills the app after a good build. */ + fun installUpdate() { + val script = updateScript + if (script == null) { + _updateStatus.value = UpdateStatus("result", + "No updater script in this install — update manually " + + "(git pull + rebuild).", url = REPO_WEB, stale = true) + return + } + _updateStatus.value = UpdateStatus("result", + "Updating: pulling from forgejo and rebuilding — the app will " + + "relaunch itself when it's done. Log: " + + "${appDir.absolutePath}/logs/update.log") + scope.launch { + runCatching { + ProcessBuilder("setsid", "nohup", script.absolutePath) + .directory(script.parentFile.parentFile) + .redirectOutput(ProcessBuilder.Redirect.DISCARD) + .start() + }.onFailure { + _updateStatus.value = UpdateStatus("result", + "Could not start the updater: ${it.message}", + url = REPO_WEB, stale = true) + } + } + } + private val _detail = MutableStateFlow(null) val detail: StateFlow = _detail.asStateFlow() diff --git a/app/src/main/kotlin/com/shonar/desktop/Screens.kt b/app/src/main/kotlin/com/shonar/desktop/Screens.kt index 9c05dcb..24fbf58 100644 --- a/app/src/main/kotlin/com/shonar/desktop/Screens.kt +++ b/app/src/main/kotlin/com/shonar/desktop/Screens.kt @@ -1451,6 +1451,11 @@ fun SettingsScreen(state: DesktopState) { Text("Open repo") } } + if (us.stale && state.updateScript != null) { + ShonarButton({ state.installUpdate() }) { + Text("Install update") + } + } } val us = state.updateStatus.collectAsState().value if (us.message.isNotEmpty()) { diff --git a/scripts/update-app.sh b/scripts/update-app.sh new file mode 100755 index 0000000..10cfd60 --- /dev/null +++ b/scripts/update-app.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Detached updater for SHONAR Desktop, spawned by the app's +# Settings → "Install update" button. Runs OUTSIDE the app process +# (setsid) so it survives the app quitting mid-update: +# 1. git pull the tracked branch from the forgejo remote +# 2. rebuild the distributable +# 3. kill the running app + engine +# 4. relaunch via the desktop launcher +# Log: ~/.config/shonar-desktop/logs/update.log +set -uo pipefail +repo="${SHONAR_REPO:-$HOME/Projects/Shonar Desktop}" +logdir="$HOME/.config/shonar-desktop/logs" +mkdir -p "$logdir" +log="$logdir/update.log" +exec >>"$log" 2>&1 +echo "=== update started $(date -Is) ===" + +cd "$repo" || { echo "no repo at $repo"; exit 1; } + +if ! git pull atitlan master; then + echo "git pull FAILED — leaving the running app alone" + exit 1 +fi + +if ! ./gradlew :app:createDistributable -q; then + echo "build FAILED — leaving the running app alone" + exit 1 +fi + +# Stop the app (and its engine) only AFTER a good build exists. +pkill -f 'com.shonar.desktop.MainKt' 2>/dev/null +pkill -f 'uvicorn shonar.main:app' 2>/dev/null +sleep 2 + +bin="$repo/app/build/compose/binaries/main/app/shonar-desktop/bin/shonar-desktop" +[ -x "$bin" ] || { echo "distributable missing at $bin"; exit 1; } +export SHONAR_REPO="$repo" +export DISPLAY="${DISPLAY:-:0}" +setsid nohup "$bin" >/dev/null 2>&1 & +echo "=== update done, relaunched $(date -Is) ==="