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 urllib.request
|
||||
import hashlib
|
||||
import fcntl
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 4000
|
||||
@@ -540,6 +541,12 @@ def download_file(url: str, dest: pathlib.Path):
|
||||
|
||||
def check_for_update():
|
||||
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["progress"] = "Checking for updates…"
|
||||
save_update_state(state)
|
||||
@@ -562,6 +569,8 @@ def check_for_update():
|
||||
state["in_progress"] = False
|
||||
state["progress"] = None
|
||||
save_update_state(state)
|
||||
if lock:
|
||||
release_lock(lock)
|
||||
return state
|
||||
|
||||
|
||||
@@ -573,6 +582,13 @@ def apply_update_stub():
|
||||
save_update_state(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
|
||||
state["in_progress"] = True
|
||||
state["progress"] = "Starting update…"
|
||||
@@ -623,6 +639,8 @@ def apply_update_stub():
|
||||
if API_PATH.exists():
|
||||
shutil.copy2(API_PATH, backup_dir / "pikit-api.py")
|
||||
|
||||
prune_backups(keep=2)
|
||||
|
||||
# Deploy from staging
|
||||
staged_web = stage_dir / "pikit-web"
|
||||
if staged_web.exists():
|
||||
@@ -676,11 +694,19 @@ def apply_update_stub():
|
||||
state["in_progress"] = False
|
||||
state["progress"] = None
|
||||
save_update_state(state)
|
||||
if lock:
|
||||
release_lock(lock)
|
||||
return state
|
||||
|
||||
|
||||
def rollback_update_stub():
|
||||
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)
|
||||
if not backups:
|
||||
state["status"] = "error"
|
||||
@@ -703,9 +729,41 @@ def rollback_update_stub():
|
||||
state["status"] = "error"
|
||||
state["message"] = f"Rollback failed: {e}"
|
||||
save_update_state(state)
|
||||
if lock:
|
||||
release_lock(lock)
|
||||
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):
|
||||
"""Minimal JSON API for the dashboard (status, services, updates, reset)."""
|
||||
def _send(self, code, data):
|
||||
|
||||
Reference in New Issue
Block a user