#!/usr/bin/env python3 import argparse import fcntl import hashlib import json import os import subprocess import sys import tempfile import urllib.request from urllib.parse import urljoin CONFIG = "/etc/ekklesia-classroom/device.conf" LOCKFILE = "/run/ekklesia-classroom-update.lock" def log(message): print(f"[ekklesia-classroom-update] {message}", flush=True) def read_config(): config = { "UPDATE_URL": "https://updates.riverofglorymn.org", "CHANNEL": "stable", } if os.path.exists(CONFIG): with open(CONFIG, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) config[key.strip()] = value.strip() return config def installed_version(): result = subprocess.run( ["dpkg-query", "-W", "-f=${Version}", "ekklesia-classroom"], capture_output=True, text=True, ) if result.returncode != 0: return "0.0.0" return result.stdout.strip() def version_is_newer(candidate, current): result = subprocess.run( ["dpkg", "--compare-versions", candidate, "gt", current] ) return result.returncode == 0 def fetch_json(url): request = urllib.request.Request( url, headers={"User-Agent": "Ekklesia-Classroom-Updater/0.1"}, ) with urllib.request.urlopen(request, timeout=30) as response: return json.load(response) def download(url, destination): request = urllib.request.Request( url, headers={"User-Agent": "Ekklesia-Classroom-Updater/0.1"}, ) with urllib.request.urlopen(request, timeout=120) as response: with open(destination, "wb") as f: while True: chunk = response.read(1024 * 1024) if not chunk: break f.write(chunk) def sha256(path): digest = hashlib.sha256() with open(path, "rb") as f: while True: chunk = f.read(1024 * 1024) if not chunk: break digest.update(chunk) return digest.hexdigest() def main(): parser = argparse.ArgumentParser() parser.add_argument( "--no-reboot", action="store_true", help="Install the update but do not reboot automatically", ) parser.add_argument( "--check", action="store_true", help="Check for an update without installing it", ) args = parser.parse_args() with open(LOCKFILE, "w", encoding="utf-8") as lock: try: fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) except BlockingIOError: log("Another update is already running.") return 0 config = read_config() base_url = config["UPDATE_URL"].rstrip("/") + "/" channel = config["CHANNEL"] manifest_url = urljoin(base_url, f"{channel}/manifest.json") current = installed_version() log(f"Current version: {current}") log(f"Channel: {channel}") log(f"Checking: {manifest_url}") try: manifest = fetch_json(manifest_url) except Exception as exc: log(f"Unable to fetch update manifest: {exc}") return 1 candidate = manifest["version"] log(f"Available version: {candidate}") if not version_is_newer(candidate, current): log("Already up to date.") return 0 if args.check: log(f"Update available: {current} -> {candidate}") return 0 package_url = urljoin(base_url, manifest["package"].lstrip("/")) expected_hash = manifest["sha256"].lower() os.makedirs("/var/cache/ekklesia-classroom", exist_ok=True) package_path = os.path.join( "/var/cache/ekklesia-classroom", os.path.basename(manifest["package"]), ) temporary_path = package_path + ".download" log(f"Downloading {package_url}") try: download(package_url, temporary_path) except Exception as exc: log(f"Download failed: {exc}") return 1 actual_hash = sha256(temporary_path) if actual_hash != expected_hash: log("SHA-256 verification FAILED.") log(f"Expected: {expected_hash}") log(f"Received: {actual_hash}") os.unlink(temporary_path) return 1 log("SHA-256 verification passed.") os.replace(temporary_path, package_path) log(f"Installing {candidate}") result = subprocess.run( [ "apt-get", "install", "-y", package_path, ] ) if result.returncode != 0: log("Package installation failed.") return result.returncode log(f"Update to {candidate} installed successfully.") if manifest.get("requires_reboot", False): if args.no_reboot: log("Update requires reboot, but --no-reboot was specified.") else: log("Update requires reboot. Rebooting now.") subprocess.run(["systemctl", "reboot"]) return 0 if __name__ == "__main__": sys.exit(main())