Installable app + one-click update from forgejo
- scripts/update-app.sh: detached updater (setsid, survives app exit): git pull atitlan master -> gradle createDistributable -> only then kill app+engine and relaunch the fresh binary. Pull or build failure leaves the running app alone. Log: ~/.config/shonar-desktop/logs/update.log. - Settings → About & updates: 'Install update' button appears when the check finds a newer remote commit; hands off to the script and tells the user the app will relaunch itself. - Desktop entry (~/.local/share/applications/shonar-desktop.desktop, outside the repo): launches the packaged distributable via launch-desktop.sh; SHONAR_REPO fallback already resolves the tree, so the packaged app spawns the engine and sees the updater script. Gate: :app:compileKotlin + :app:test green; desktop-file-validate clean; launcher verified (app window from the menu path); updater fail-safe verified (dirty-tree pull refused, app survived).
This commit is contained in:
parent
b4e335d720
commit
fc5502a7cd
3 changed files with 81 additions and 0 deletions
|
|
@ -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<DetailUi?>(null)
|
private val _detail = MutableStateFlow<DetailUi?>(null)
|
||||||
val detail: StateFlow<DetailUi?> = _detail.asStateFlow()
|
val detail: StateFlow<DetailUi?> = _detail.asStateFlow()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1451,6 +1451,11 @@ fun SettingsScreen(state: DesktopState) {
|
||||||
Text("Open repo")
|
Text("Open repo")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (us.stale && state.updateScript != null) {
|
||||||
|
ShonarButton({ state.installUpdate() }) {
|
||||||
|
Text("Install update")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val us = state.updateStatus.collectAsState().value
|
val us = state.updateStatus.collectAsState().value
|
||||||
if (us.message.isNotEmpty()) {
|
if (us.message.isNotEmpty()) {
|
||||||
|
|
|
||||||
40
scripts/update-app.sh
Executable file
40
scripts/update-app.sh
Executable file
|
|
@ -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) ==="
|
||||||
Loading…
Add table
Add a link
Reference in a new issue