Add locking, backup pruning, and systemd timer for release checks
This commit is contained in:
58
pikit-api.py
58
pikit-api.py
@@ -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):
|
||||||
|
|||||||
14
systemd/pikit-update.service
Normal file
14
systemd/pikit-update.service
Normal 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
|
||||||
11
systemd/pikit-update.timer
Normal file
11
systemd/pikit-update.timer
Normal 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
|
||||||
Reference in New Issue
Block a user