mirror of
https://github.com/Cogential/frametanium.git
synced 2026-10-23 16:00:35 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e1564df68 |
No files matched your search
@@ -50,7 +50,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
run: |
|
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 -C dist/bundle -czf frametanium-steam-frame-arm64.tar.gz .
|
||||||
tar -tzf frametanium-steam-frame-arm64.tar.gz | grep -v '^./art/'
|
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
|
`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
|
`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.
|
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
|
- Write settings files only with `save_json` (unique temp + fsync; `private=True` for config.json) and do
|
||||||
read-modify-writes under `files_lock`.
|
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
|
- 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
|
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
|
`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
|
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.
|
updates, **B** back/close.
|
||||||
- **From a phone or PC:** `http://<frame-ip>:8643` (add apps, edit name/install style/program, remove).
|
- **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
|
**Frametanium updates itself:** when a check finds a newer [release](https://github.com/Cogential/frametanium/releases)
|
||||||
[release](https://github.com/Cogential/frametanium/releases), reinstalls the service and the headset app, and
|
of Frametanium, it installs it without asking (unless something else is installing; the next check tries again):
|
||||||
restarts the service a few seconds later (reopen the headset app to get the new one).
|
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
|
## Files
|
||||||
- `catalog.json`: the built-in ports.
|
- `catalog.json`: the built-in ports.
|
||||||
|
|||||||
@@ -801,7 +801,7 @@ int main(void) {
|
|||||||
static Found fd;
|
static Found fd;
|
||||||
static Disc disc[MAX_DISC];
|
static Disc disc[MAX_DISC];
|
||||||
int napps = -1, sel = 0, nrels = 0, rsel = 0, msel = 0, asel = 0, nsug = 0, tried_start = 0;
|
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] = "";
|
char disc_msg[300] = "";
|
||||||
Uint32 next_disc_poll = 0;
|
Uint32 next_disc_poll = 0;
|
||||||
int screen = LIST, kb_row = 1, kb_col = 0, notice_busy = 0;
|
int screen = LIST, kb_row = 1, kb_col = 0, notice_busy = 0;
|
||||||
@@ -863,6 +863,10 @@ int main(void) {
|
|||||||
tried_start = 1;
|
tried_start = 1;
|
||||||
try_start_service();
|
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;
|
int busy = notice_busy;
|
||||||
for (int i = 0; i < napps; i++) busy |= busy_status(apps[i].status);
|
for (int i = 0; i < napps; i++) busy |= busy_status(apps[i].status);
|
||||||
next_poll = SDL_GetTicks() + (busy ? 400 : 2000);
|
next_poll = SDL_GetTicks() + (busy ? 400 : 2000);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ done
|
|||||||
|
|
||||||
mkdir -p "$dest"
|
mkdir -p "$dest"
|
||||||
cp "$here/manager.py" "$here/page.html" "$here/catalog.json" "$here/suggestions.json" "$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"
|
rm -rf "$dest/art" && cp -r "$here/art" "$dest/art"
|
||||||
cp "$here/frametanium.service" "$HOME/.config/systemd/user/"
|
cp "$here/frametanium.service" "$HOME/.config/systemd/user/"
|
||||||
systemctl --user daemon-reload
|
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)
|
"github_token": "", # needed only for private repos (read-only access to their releases is enough)
|
||||||
"compat_tool": "SteamLinuxRuntime_4-arm64",
|
"compat_tool": "SteamLinuxRuntime_4-arm64",
|
||||||
"check_hours": 6,
|
"check_hours": 6,
|
||||||
|
"self_update": True, # install new Frametanium releases by itself
|
||||||
}
|
}
|
||||||
CHUNK = 1024 * 1024
|
CHUNK = 1024 * 1024
|
||||||
RELEASES_TTL = 600
|
RELEASES_TTL = 600
|
||||||
@@ -1149,10 +1150,11 @@ def remove(pid, delete_files=True):
|
|||||||
|
|
||||||
|
|
||||||
def update_all(_pid=None):
|
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")
|
jobs.set("_list", "installing", 0.0, "Updating everything")
|
||||||
count = 0
|
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":
|
if row["status"] == "update available":
|
||||||
install(row["id"])
|
install(row["id"])
|
||||||
count += 1
|
count += 1
|
||||||
@@ -1175,6 +1177,34 @@ def start(fn, pid, *args):
|
|||||||
_latest = {} # app id -> (tag, error)
|
_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):
|
def refresh(force=False):
|
||||||
"""Checks every app for releases. Returns False if any check failed."""
|
"""Checks every app for releases. Returns False if any check failed."""
|
||||||
ok = True
|
ok = True
|
||||||
@@ -1195,6 +1225,8 @@ def app_state():
|
|||||||
folder = target_dir(port, mode) if mode != "auto" else None
|
folder = target_dir(port, mode) if mode != "auto" else None
|
||||||
present = bool(folder and folder.exists())
|
present = bool(folder and folder.exists())
|
||||||
tag = installed.get(pid, {}).get("tag") if present else None
|
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:
|
if present and not tag:
|
||||||
tag = "unknown"
|
tag = "unknown"
|
||||||
if pid not in _latest:
|
if pid not in _latest:
|
||||||
@@ -1412,8 +1444,13 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
arg = lambda k: q.get(k, [""])[0]
|
arg = lambda k: q.get(k, [""])[0]
|
||||||
pid = arg("id")
|
pid = arg("id")
|
||||||
if url.path == "/api/refresh":
|
if url.path == "/api/refresh":
|
||||||
_releases_cache.clear()
|
# The headset app asks with launch=1 each time it opens: releases checked in the last 10 minutes count.
|
||||||
threading.Thread(target=refresh, kwargs={"force": True}, daemon=True).start()
|
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})
|
self.reply({"ok": True})
|
||||||
elif url.path == "/api/update_all":
|
elif url.path == "/api/update_all":
|
||||||
ok = start(update_all, "_list")
|
ok = start(update_all, "_list")
|
||||||
@@ -1487,7 +1524,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
def checker():
|
def checker():
|
||||||
retry = 30
|
retry = 30
|
||||||
while True:
|
while True:
|
||||||
if refresh(force=True):
|
if check(force=True):
|
||||||
retry = 30
|
retry = 30
|
||||||
discover()
|
discover()
|
||||||
time.sleep(max(1, float(config().get("check_hours", 6))) * 3600)
|
time.sleep(max(1, float(config().get("check_hours", 6))) * 3600)
|
||||||
@@ -1501,6 +1538,13 @@ def checker():
|
|||||||
def main():
|
def main():
|
||||||
ensure_secrets()
|
ensure_secrets()
|
||||||
cfg = config()
|
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()
|
threading.Thread(target=checker, daemon=True).start()
|
||||||
server = ThreadingHTTPServer(("0.0.0.0", int(cfg["port"])), Handler)
|
server = ThreadingHTTPServer(("0.0.0.0", int(cfg["port"])), Handler)
|
||||||
server.daemon_threads = True
|
server.daemon_threads = True
|
||||||
|
|||||||
@@ -8,5 +8,7 @@ APPDIR="$1" OUT="$2"
|
|||||||
rm -rf "$OUT" && mkdir -p "$OUT/tools"
|
rm -rf "$OUT" && mkdir -p "$OUT/tools"
|
||||||
cp manager.py page.html catalog.json suggestions.json frametanium.service install.sh README.md "$OUT/"
|
cp manager.py page.html catalog.json suggestions.json frametanium.service install.sh README.md "$OUT/"
|
||||||
cp tools/find_appid.py "$OUT/tools/"
|
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 art "$OUT/art"
|
||||||
cp -r "$APPDIR" "$OUT/app"
|
cp -r "$APPDIR" "$OUT/app"
|
||||||
Reference in new issue
Block a user