mirror of
https://github.com/Cogential/frametanium.git
synced 2026-10-23 19:00:36 +02:00
Check for updates when the headset app opens, and let Frametanium update itself
The headset app asks for a check (cached releases under 10 minutes old count) each time it opens; every check
then installs a newer Frametanium release without asking unless something else is installing
("self_update": false turns it off). Its version now comes from a VERSION file in the bundle, so its own row
is right however it was installed (deploy.sh included), and Update all does Frametanium last so the restart
can't cut off another install. After an update the list says to reopen the app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3G4bF9bag9rHEh1Anvngi
This commit is contained in:
1 parent
80a4e62a49
commit
1e1564df68
7 files changed
+71
-12
No files matched your search
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
tools/bundle.sh dist/app dist/bundle
|
||||
VERSION="$GITHUB_REF_NAME" tools/bundle.sh dist/app dist/bundle
|
||||
tar -C dist/bundle -czf frametanium-steam-frame-arm64.tar.gz .
|
||||
tar -tzf frametanium-steam-frame-arm64.tar.gz | grep -v '^./art/'
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@ See README.md for what it does. Service: `manager.py` (stdlib Python, runs on th
|
||||
`ensure_secrets()`. Never add a loopback/no-password bypass, never accept secrets in the query string, and keep
|
||||
`host_ok()` (Host allow-list + Origin check). Any auth change must change app.c and page.html together, and old
|
||||
headset apps must still get a readable "#" notice in TSV replies.
|
||||
- **Self-update:** `check()` (every few hours, on each headset-app launch via `/api/refresh?launch=1`, and on
|
||||
request) installs a newer Frametanium release by itself (`self_update` in config.json, default on). Its installed
|
||||
version is the bundle's `VERSION` file (`tools/bundle.sh`; CI passes `VERSION=$GITHUB_REF_NAME`), not
|
||||
installed.json. `update_all` must keep Frametanium last (its install.sh restarts the service 3 s later).
|
||||
A bad release spreads to every Frame on its own now: test before tagging.
|
||||
- Write settings files only with `save_json` (unique temp + fsync; `private=True` for config.json) and do
|
||||
read-modify-writes under `files_lock`.
|
||||
- Test locally: run manager.py with `HOME=<scratch>` and a config.json with another port; build app.c natively
|
||||
@@ -21,4 +26,4 @@ Left, in this order: security MEDIUM 3 (Discover installs run install.sh without
|
||||
check: verify the GitHub asset `digest`; plan was an ed25519 signature for Frametanium's own updates, verified with
|
||||
`openssl pkeyutl -rawin`), 6 (tar symlink traversal, .tar.zst via GNU tar), 5 (workflow: SDL2 hash, pin
|
||||
actions by SHA), 7 (AppImage executed to unpack: prefer `unsquashfs -o <ELF end>`), LOW 8/9/11/14; then code-review
|
||||
bugs 1-9 and 12-16 (2: update Frametanium last in update_all; 4: Frametanium's own row needs a VERSION file).
|
||||
bugs 1, 3, 5-9 and 12-16 (2 and 4 were fixed with the self-update on 2026-10-21).
|
||||
@@ -34,11 +34,14 @@ and Frametanium keeps it up to date. Works from the headset or a browser.
|
||||
updates, **B** back/close.
|
||||
- **From a phone or PC:** `http://<frame-ip>:8643` (add apps, edit name/install style/program, remove).
|
||||
|
||||
Updates are checked every 6 hours and on demand.
|
||||
Updates are checked every 6 hours, each time you open the headset app, and on demand.
|
||||
|
||||
**Frametanium updates itself:** it's in its own list. Updating it downloads its latest
|
||||
[release](https://github.com/Cogential/frametanium/releases), reinstalls the service and the headset app, and
|
||||
restarts the service a few seconds later (reopen the headset app to get the new one).
|
||||
**Frametanium updates itself:** when a check finds a newer [release](https://github.com/Cogential/frametanium/releases)
|
||||
of Frametanium, it installs it without asking (unless something else is installing; the next check tries again):
|
||||
it reinstalls the service and the headset app and restarts the service a few seconds later. The headset app then
|
||||
says to close and reopen it to get the new version. Turn this off with `"self_update": false` in
|
||||
`~/.config/frametanium/config.json` (you can still update it from its row in the list). Its version comes from the
|
||||
`VERSION` file the bundle carries (`tools/bundle.sh`; releases set it from the tag).
|
||||
|
||||
## Files
|
||||
- `catalog.json`: the built-in ports.
|
||||
|
||||
@@ -801,7 +801,7 @@ int main(void) {
|
||||
static Found fd;
|
||||
static Disc disc[MAX_DISC];
|
||||
int napps = -1, sel = 0, nrels = 0, rsel = 0, msel = 0, asel = 0, nsug = 0, tried_start = 0;
|
||||
int ndisc = 0, dsel = 0, disc_running = 0, disc_new = 0, found_from = ADDING;
|
||||
int ndisc = 0, dsel = 0, disc_running = 0, disc_new = 0, found_from = ADDING, launch_checked = 0;
|
||||
char disc_msg[300] = "";
|
||||
Uint32 next_disc_poll = 0;
|
||||
int screen = LIST, kb_row = 1, kb_col = 0, notice_busy = 0;
|
||||
@@ -863,6 +863,10 @@ int main(void) {
|
||||
tried_start = 1;
|
||||
try_start_service();
|
||||
}
|
||||
if (napps >= 0 && !launch_checked) { /* check for updates (and let Frametanium update itself) on opening */
|
||||
launch_checked = 1;
|
||||
post("/api/refresh?launch=1", NULL, 0);
|
||||
}
|
||||
int busy = notice_busy;
|
||||
for (int i = 0; i < napps; i++) busy |= busy_status(apps[i].status);
|
||||
next_poll = SDL_GetTicks() + (busy ? 400 : 2000);
|
||||
|
||||
@@ -20,6 +20,7 @@ done
|
||||
|
||||
mkdir -p "$dest"
|
||||
cp "$here/manager.py" "$here/page.html" "$here/catalog.json" "$here/suggestions.json" "$dest/"
|
||||
if [ -f "$here/VERSION" ]; then cp "$here/VERSION" "$dest/"; else rm -f "$dest/VERSION"; fi
|
||||
rm -rf "$dest/art" && cp -r "$here/art" "$dest/art"
|
||||
cp "$here/frametanium.service" "$HOME/.config/systemd/user/"
|
||||
systemctl --user daemon-reload
|
||||
|
||||
+49
-5
@@ -56,6 +56,7 @@ DEFAULT_CONFIG = {
|
||||
"github_token": "", # needed only for private repos (read-only access to their releases is enough)
|
||||
"compat_tool": "SteamLinuxRuntime_4-arm64",
|
||||
"check_hours": 6,
|
||||
"self_update": True, # install new Frametanium releases by itself
|
||||
}
|
||||
CHUNK = 1024 * 1024
|
||||
RELEASES_TTL = 600
|
||||
@@ -1149,10 +1150,11 @@ def remove(pid, delete_files=True):
|
||||
|
||||
|
||||
def update_all(_pid=None):
|
||||
"""Updates every installed app that has a newer release, one after another."""
|
||||
"""Updates every installed app that has a newer release, one after another. Frametanium goes last: updating it
|
||||
restarts the service a few seconds later, which would stop any install after it."""
|
||||
jobs.set("_list", "installing", 0.0, "Updating everything")
|
||||
count = 0
|
||||
for row in app_state():
|
||||
for row in sorted(app_state(), key=lambda r: r["id"] == SELF_ID):
|
||||
if row["status"] == "update available":
|
||||
install(row["id"])
|
||||
count += 1
|
||||
@@ -1175,6 +1177,34 @@ def start(fn, pid, *args):
|
||||
_latest = {} # app id -> (tag, error)
|
||||
|
||||
|
||||
SELF_ID = "frametanium"
|
||||
|
||||
|
||||
def own_version():
|
||||
"""The release this service came from (VERSION, written by tools/bundle.sh), or None."""
|
||||
try:
|
||||
return (APP_DIR / "VERSION").read_text().strip() or None
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def self_update():
|
||||
"""Installs a newer Frametanium release once a check has found one, unless something else is installing (the next
|
||||
check tries again) or "self_update" is off in config.json."""
|
||||
if not config().get("self_update", True):
|
||||
return
|
||||
row = next((r for r in app_state() if r["id"] == SELF_ID), None)
|
||||
if row and row["status"] == "update available" and start(install, SELF_ID):
|
||||
print(f"Updating Frametanium {row['installed']} -> {row['latest']}", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def check(force=True):
|
||||
"""An update check (every few hours, when the headset app opens, or on request), then the self-update."""
|
||||
ok = refresh(force=force)
|
||||
self_update()
|
||||
return ok
|
||||
|
||||
|
||||
def refresh(force=False):
|
||||
"""Checks every app for releases. Returns False if any check failed."""
|
||||
ok = True
|
||||
@@ -1195,6 +1225,8 @@ def app_state():
|
||||
folder = target_dir(port, mode) if mode != "auto" else None
|
||||
present = bool(folder and folder.exists())
|
||||
tag = installed.get(pid, {}).get("tag") if present else None
|
||||
if pid == SELF_ID: # it's running, however it was installed (deploy.sh, its install.sh or an update)
|
||||
present, tag = True, own_version() or tag
|
||||
if present and not tag:
|
||||
tag = "unknown"
|
||||
if pid not in _latest:
|
||||
@@ -1412,8 +1444,13 @@ class Handler(BaseHTTPRequestHandler):
|
||||
arg = lambda k: q.get(k, [""])[0]
|
||||
pid = arg("id")
|
||||
if url.path == "/api/refresh":
|
||||
_releases_cache.clear()
|
||||
threading.Thread(target=refresh, kwargs={"force": True}, daemon=True).start()
|
||||
# The headset app asks with launch=1 each time it opens: releases checked in the last 10 minutes count.
|
||||
force = arg("launch") != "1"
|
||||
if force:
|
||||
_releases_cache.clear()
|
||||
elif jobs.state.get("_list", {}).get("message", "").startswith("Frametanium updated to"):
|
||||
jobs.state.pop("_list", None) # it's been reopened
|
||||
threading.Thread(target=check, kwargs={"force": force}, daemon=True).start()
|
||||
self.reply({"ok": True})
|
||||
elif url.path == "/api/update_all":
|
||||
ok = start(update_all, "_list")
|
||||
@@ -1487,7 +1524,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
def checker():
|
||||
retry = 30
|
||||
while True:
|
||||
if refresh(force=True):
|
||||
if check(force=True):
|
||||
retry = 30
|
||||
discover()
|
||||
time.sleep(max(1, float(config().get("check_hours", 6))) * 3600)
|
||||
@@ -1501,6 +1538,13 @@ def checker():
|
||||
def main():
|
||||
ensure_secrets()
|
||||
cfg = config()
|
||||
version = own_version()
|
||||
if version and version != cfg.get("last_version"):
|
||||
if cfg.get("last_version"):
|
||||
jobs.set("_list", "done", 1.0, f"Frametanium updated to {version}. Close this app and open it again "
|
||||
"to use the new version.")
|
||||
with files_lock:
|
||||
save_json(CONFIG_FILE, {**load_json(CONFIG_FILE, {}), "last_version": version}, private=True)
|
||||
threading.Thread(target=checker, daemon=True).start()
|
||||
server = ThreadingHTTPServer(("0.0.0.0", int(cfg["port"])), Handler)
|
||||
server.daemon_threads = True
|
||||
|
||||
@@ -8,5 +8,7 @@ APPDIR="$1" OUT="$2"
|
||||
rm -rf "$OUT" && mkdir -p "$OUT/tools"
|
||||
cp manager.py page.html catalog.json suggestions.json frametanium.service install.sh README.md "$OUT/"
|
||||
cp tools/find_appid.py "$OUT/tools/"
|
||||
# The release it is (releases pass VERSION; local builds count as the newest tag), so Frametanium knows when to update.
|
||||
echo "${VERSION:-$(git describe --tags --abbrev=0 2>/dev/null || echo unknown)}" > "$OUT/VERSION"
|
||||
cp -r art "$OUT/art"
|
||||
cp -r "$APPDIR" "$OUT/app"
|
||||
Reference in new issue
Block a user