Add built-in updater, bump to 1.2.0

- predatord checks the Gitea releases every 6 hours, downloads the
  newest pacman package, verifies its SHA-256 from the release notes
  and installs it with `pacman -U` in a separate systemd unit
- Update notice and install button on the System page, in the sidebar
  and in the panel widget; `predatorctl update [--check] [--yes]`
- Optional API token for private repositories
- packaging/release.py builds and publishes releases in the format
  the updater expects

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 17:42:02 +02:00
co-authored by Claude Opus 5.5
parent 2ef62e85cd
commit cc4fdb9cd0
14 changed files with 376 additions and 9 deletions
+49
View File
@@ -24,6 +24,7 @@ import traceback
from . import CONFIG_DIR, CONFIG_PATH, SOCKET_PATH, STATE_PATH, __version__
from . import hardware as hw
from . import updater
from .keyboard import EFFECTS, NUM_CELLS, Keyboard
DEFAULT_CONFIG = {
@@ -47,6 +48,7 @@ DEFAULT_CONFIG = {
},
"battery": {"limit": None}, # None = leave the firmware state alone
"keyboard_presets": {},
"updates": {"auto_check": True, "interval_hours": 6},
}
@@ -93,6 +95,9 @@ class Daemon:
self.state = {}
self.battery_limit = None
self._ticks = 0
self.update_info = None
self.update_error = None
self._update_checking = False
# ---------------------------------------------------------- Configuration
def _load_config(self):
@@ -230,6 +235,9 @@ class Daemon:
"lightbar_effective": self.lightbar_effective(),
"fan_curve_applied": self.curve_applied,
"battery_limit": self.battery_limit,
"update": self.update_info,
"update_error": self.update_error,
"update_status": updater.status(),
"available": {
"keyboard": self.kbd_present,
"wmi": hw.wmi_available(hw.GUID_GAMING),
@@ -241,6 +249,34 @@ class Daemon:
}
return st
def check_update_async(self):
if self._update_checking:
return
self._update_checking = True
def work():
try:
info = updater.check()
if info["available"] and (self.update_info or {}).get("latest") != info["latest"]:
log("Update available: %s -> %s" % (info["current"], info["latest"]))
self.update_info, self.update_error = info, None
except Exception as e:
self.update_error = str(e)
finally:
self._update_checking = False
threading.Thread(target=work, daemon=True).start()
def maybe_check_update(self):
u = self.config.get("updates", {})
if not u.get("auto_check", True):
return
last = (self.update_info or {}).get("checked", 0)
if time.time() - last > float(u.get("interval_hours", 6)) * 3600:
if self.update_error and time.time() - getattr(self, "_last_try", 0) < 900:
return # offline: retry every 15 min
self._last_try = time.time()
self.check_update_async()
def loop(self):
while True:
try:
@@ -261,6 +297,8 @@ class Daemon:
self.last_ac = ac
self.fan_curve_tick()
self._ticks += 1
if self._ticks % 20 == 1:
self.maybe_check_update()
if self._ticks % 40 == 0:
self.refresh_battery_limit()
self.state = self.build_state()
@@ -279,6 +317,17 @@ class Daemon:
if cmd == "state":
self.state = self.build_state()
return self.state
if cmd == "check_update":
self.update_info, self.update_error = updater.check(), None
return self.update_info
if cmd == "install_update":
info = updater.check()
self.update_info = info
return updater.install(info)
if cmd == "set_updates":
merge(self.config["updates"], {k: a[k] for k in ("auto_check", "interval_hours") if k in a})
self.save_config()
return {}
if cmd == "restore":
self.restore()
return {}