init
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# bt-update - OTA update with automatic rollback for ButtonTask.
|
||||
#
|
||||
# Usage:
|
||||
# bt-update stage <package.tar.gz> # deploy + health-check + commit/rollback
|
||||
# bt-update rollback # revert to the previous version
|
||||
#
|
||||
# Layout (see paths.py):
|
||||
# $ROOT/versions/<ver>/ deployed versions (ButtonTask + webconfig/)
|
||||
# $ROOT/current -> versions/<ver>
|
||||
# $ROOT/run/update-status.json progress for the web UI
|
||||
# $ROOT/run/qt.alive heartbeat written by the Qt UI
|
||||
# $ROOT/.previous path of the previously active version
|
||||
#
|
||||
# Designed to be launched detached (setsid) via sudo by the web service, so it
|
||||
# survives the service restart performed mid-update.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="${BUTTONTASK_ROOT:-/opt/buttontask}"
|
||||
VERSIONS="$ROOT/versions"
|
||||
CURRENT="$ROOT/current"
|
||||
RUN="$ROOT/run"
|
||||
STATUS="$RUN/update-status.json"
|
||||
HEARTBEAT="$RUN/qt.alive"
|
||||
PREV_MARK="$ROOT/.previous"
|
||||
HEALTH_TIMEOUT="${BUTTONTASK_HEALTH_TIMEOUT:-30}"
|
||||
|
||||
mkdir -p "$RUN" "$VERSIONS"
|
||||
|
||||
write_status() {
|
||||
local state="$1"; local msg="${2:-}"
|
||||
msg="${msg//\"/}"
|
||||
printf '{"state":"%s","message":"%s","ts":%s}\n' "$state" "$msg" "$(date +%s)" > "$STATUS"
|
||||
}
|
||||
|
||||
web_port() {
|
||||
python3 - <<'PY' 2>/dev/null || echo 8080
|
||||
import json, os
|
||||
root = os.environ.get("BUTTONTASK_ROOT", "/opt/buttontask")
|
||||
try:
|
||||
with open(os.path.join(root, "config", "config.json")) as f:
|
||||
print(int(json.load(f).get("settings", {}).get("webPort", 8080)))
|
||||
except Exception:
|
||||
print(8080)
|
||||
PY
|
||||
}
|
||||
|
||||
restart_services() {
|
||||
systemctl restart buttontask-web buttontask 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Returns 0 when the freshly started version looks healthy.
|
||||
health_check() {
|
||||
local port deadline now hb_age
|
||||
port="$(web_port)"
|
||||
deadline=$(( $(date +%s) + HEALTH_TIMEOUT ))
|
||||
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||||
# 1) web answers /healthz
|
||||
if curl -fsS "http://127.0.0.1:${port}/healthz" >/dev/null 2>&1; then
|
||||
# 2) Qt UI heartbeat is fresh (file touched in the last 20s)
|
||||
if [ -f "$HEARTBEAT" ]; then
|
||||
now=$(date +%s)
|
||||
hb_age=$(( now - $(stat -c %Y "$HEARTBEAT" 2>/dev/null || echo 0) ))
|
||||
if [ "$hb_age" -le 20 ]; then
|
||||
# 3) Qt service is active
|
||||
if systemctl is-active --quiet buttontask 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
switch_to() {
|
||||
# Atomically repoint the current symlink.
|
||||
ln -sfn "$1" "$CURRENT"
|
||||
}
|
||||
|
||||
cmd_stage() {
|
||||
local pkg="$1"
|
||||
[ -f "$pkg" ] || { write_status error "package not found"; exit 1; }
|
||||
|
||||
write_status staging "распаковка"
|
||||
local tmp ver dest
|
||||
tmp="$(mktemp -d "$VERSIONS/.stage.XXXXXX")"
|
||||
if ! tar -xzf "$pkg" -C "$tmp"; then
|
||||
rm -rf "$tmp"; write_status error "bad archive"; exit 1
|
||||
fi
|
||||
# Some packages wrap content in a single top dir; flatten if so.
|
||||
if [ ! -f "$tmp/manifest.json" ]; then
|
||||
local inner
|
||||
inner="$(find "$tmp" -maxdepth 2 -name manifest.json | head -n1)"
|
||||
[ -n "$inner" ] && tmp="$(dirname "$inner")"
|
||||
fi
|
||||
[ -f "$tmp/manifest.json" ] || { rm -rf "$tmp"; write_status error "manifest.json missing"; exit 1; }
|
||||
[ -x "$tmp/ButtonTask" ] || chmod +x "$tmp/ButtonTask" 2>/dev/null || true
|
||||
|
||||
ver="$(python3 -c "import json,sys;print(json.load(open('$tmp/manifest.json')).get('version','unknown'))" 2>/dev/null || echo unknown)"
|
||||
dest="$VERSIONS/$ver"
|
||||
|
||||
# Remember the current version for rollback.
|
||||
if [ -L "$CURRENT" ]; then
|
||||
readlink -f "$CURRENT" > "$PREV_MARK"
|
||||
fi
|
||||
|
||||
# Avoid clobbering the running version directory.
|
||||
if [ "$(readlink -f "$CURRENT" 2>/dev/null)" = "$(readlink -f "$dest" 2>/dev/null)" ]; then
|
||||
dest="${dest}-$(date +%s)"
|
||||
fi
|
||||
rm -rf "$dest"
|
||||
mv "$tmp" "$dest"
|
||||
chown -R buttontask:buttontask "$dest" 2>/dev/null || true
|
||||
rm -f "$pkg"
|
||||
|
||||
write_status switching "$ver"
|
||||
switch_to "$dest"
|
||||
restart_services
|
||||
|
||||
write_status health_check "$ver"
|
||||
if health_check; then
|
||||
write_status success "$ver"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Rollback.
|
||||
if [ -s "$PREV_MARK" ]; then
|
||||
switch_to "$(cat "$PREV_MARK")"
|
||||
restart_services
|
||||
write_status rolledback "запуск $ver не удался"
|
||||
else
|
||||
write_status error "health-check failed, no previous version"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
cmd_rollback() {
|
||||
[ -s "$PREV_MARK" ] || { write_status error "no previous version"; exit 1; }
|
||||
write_status switching "rollback"
|
||||
switch_to "$(cat "$PREV_MARK")"
|
||||
restart_services
|
||||
write_status rollback_done "откат выполнен"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
stage) [ "$#" -ge 2 ] || { echo "usage: bt-update stage <pkg>"; exit 2; }; cmd_stage "$2" ;;
|
||||
rollback) cmd_rollback ;;
|
||||
*) echo "usage: bt-update {stage <pkg>|rollback}"; exit 2 ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user