Add locking, backup pruning, and systemd timer for release checks

This commit is contained in:
Aaron
2025-12-10 19:43:00 -05:00
parent 648b1c6643
commit d6f7b92fb4
3 changed files with 83 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
import re import re
import urllib.request import urllib.request
import hashlib import hashlib
import fcntl
HOST = "127.0.0.1" HOST = "127.0.0.1"
PORT = 4000 PORT = 4000
@@ -540,6 +541,12 @@ def download_file(url: str, dest: pathlib.Path):
def check_for_update(): def check_for_update():
state = load_update_state() state = load_update_state()
lock = acquire_lock()
if lock is None:
state["status"] = "error"
state["message"] = "Another update is running"
save_update_state(state)
return state
state["in_progress"] = True state["in_progress"] = True
state["progress"] = "Checking for updates…" state["progress"] = "Checking for updates…"
save_update_state(state) save_update_state(state)
@@ -562,6 +569,8 @@ def check_for_update():
state["in_progress"] = False state["in_progress"] = False
state["progress"] = None state["progress"] = None
save_update_state(state) save_update_state(state)
if lock:
release_lock(lock)
return state return state
@@ -573,6 +582,13 @@ def apply_update_stub():
save_update_state(state) save_update_state(state)
return state return state
lock = acquire_lock()
if lock is None:
state["status"] = "error"
state["message"] = "Another update is running"
save_update_state(state)
return state
manifest = None manifest = None
state["in_progress"] = True state["in_progress"] = True
state["progress"] = "Starting update…" state["progress"] = "Starting update…"
@@ -623,6 +639,8 @@ def apply_update_stub():
if API_PATH.exists(): if API_PATH.exists():
shutil.copy2(API_PATH, backup_dir / "pikit-api.py") shutil.copy2(API_PATH, backup_dir / "pikit-api.py")
prune_backups(keep=2)
# Deploy from staging # Deploy from staging
staged_web = stage_dir / "pikit-web" staged_web = stage_dir / "pikit-web"
if staged_web.exists(): if staged_web.exists():
@@ -676,11 +694,19 @@ def apply_update_stub():
state["in_progress"] = False state["in_progress"] = False
state["progress"] = None state["progress"] = None
save_update_state(state) save_update_state(state)
if lock:
release_lock(lock)
return state return state
def rollback_update_stub(): def rollback_update_stub():
state = load_update_state() state = load_update_state()
lock = acquire_lock()
if lock is None:
state["status"] = "error"
state["message"] = "Another update is running"
save_update_state(state)
return state
backups = sorted(BACKUP_ROOT.glob("*"), reverse=True) backups = sorted(BACKUP_ROOT.glob("*"), reverse=True)
if not backups: if not backups:
state["status"] = "error" state["status"] = "error"
@@ -703,9 +729,41 @@ def rollback_update_stub():
state["status"] = "error" state["status"] = "error"
state["message"] = f"Rollback failed: {e}" state["message"] = f"Rollback failed: {e}"
save_update_state(state) save_update_state(state)
if lock:
release_lock(lock)
return state return state
def acquire_lock():
try:
ensure_dir(UPDATE_LOCK.parent)
lockfile = UPDATE_LOCK.open("w")
fcntl.flock(lockfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
lockfile.write(str(os.getpid()))
lockfile.flush()
return lockfile
except Exception:
return None
def release_lock(lockfile):
try:
fcntl.flock(lockfile.fileno(), fcntl.LOCK_UN)
lockfile.close()
UPDATE_LOCK.unlink(missing_ok=True)
except Exception:
pass
def prune_backups(keep: int = 2):
if keep < 1:
keep = 1
ensure_dir(BACKUP_ROOT)
backups = sorted([p for p in BACKUP_ROOT.iterdir() if p.is_dir()], reverse=True)
for old in backups[keep:]:
shutil.rmtree(old, ignore_errors=True)
class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):
"""Minimal JSON API for the dashboard (status, services, updates, reset).""" """Minimal JSON API for the dashboard (status, services, updates, reset)."""
def _send(self, code, data): def _send(self, code, data):

View File

@@ -0,0 +1,14 @@
[Unit]
Description=Pi-Kit release update check/apply helper
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/curl -s -H "Content-Type: application/json" -X POST http://127.0.0.1/api/update/check
User=root
Group=root
TimeoutStartSec=300
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Daily Pi-Kit release update check
[Timer]
OnCalendar=*-*-* 04:20:00
RandomizedDelaySec=900
Persistent=true
Unit=pikit-update.service
[Install]
WantedBy=timers.target