mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
Two gaps that both end the same way — a panel nobody can fix without a van. OFFLINE. Content bytes were never persistently cached. The service worker skipped /uploads/content/ and leaned on the browser's HTTP cache, which is reasonable on a desktop and is not a documented-persistent store here: BrightSign guarantees survival across reboots for IndexedDB, localStorage and SQLite, and their own answer for offline video is to cache the bytes explicitly. A panel could come back from a power cut with its playlist intact — that lives in localStorage — and no media to play it with. The reason content was skipped is real, and player-cache-policy.js is what makes intercepting it safe. Seeking video issues range requests, and naive caching is worse than none: storing a 206 as the whole file means every later full request gets a fragment, and answering a range request with a 200 makes some media stacks fail outright. So only complete 200s are stored, and ranges are served by slicing the stored body into a correct 206. The content cache survives shell re-versioning, or every deploy would re-download the playlist over a link that may be exactly what is broken. SELF-UPDATE. The package can replace autorun.brs, so a truncated file is a dark panel with no app underneath. The safety is the ordering: download to .part, verify sha256 AND size, then delete the .done marker, rename, reboot. Marker first is not stylistic — leaving it makes the next boot skip the archive and the update silently never happens. A failed extract parks the zip as .bad instead of retrying every boot, which would be a loop indistinguishable from a hardware fault. sha256 because that is what roMessageDigest can compute; a checksum the player cannot verify is an unverifiable package. The decision lives on the server and is unit-tested, and the host only executes it — re-implementing the version comparison in BrightScript would put the prerelease trap somewhere untestable. That trap is honoured directly: a player on 1.9.29-rc1 is running something semver-OLDER than 1.9.29, so an opted-in player HOLDS a prerelease of its own core rather than being pulled off the build it was given to test. Narrowly — a newer core still lands, so opting in never means never updating again. Both loop conditions are closed by construction. The manifest and the download come from one buffer hashed once, so a checksum cannot describe bytes we are not serving. And the version is stamped into autorun.brs at build time by both builders, so the script reports the version it actually is — otherwise the player applies the update, still reports the old version, and is offered the same package forever. Failure always degrades to "keep running the old version": an unreachable manifest, a missing checksum, a failed verification, a full attempt counter and an unbuildable package all resolve to skip. 998 tests pass (was 954).
98 lines
4 KiB
Bash
Executable file
98 lines
4 KiB
Bash
Executable file
#!/bin/bash
|
|
# Build brightsign/autorun.zip — the single-file installer for a BrightSign player.
|
|
#
|
|
# scripts/build-autorun-zip.sh [--server https://your-server] [-o path/to/autorun.zip]
|
|
#
|
|
# Drop the resulting autorun.zip on the root of a player's storage (microSD, USB, or internal
|
|
# flash over SFTP) and power-cycle. autozip.brs unpacks it in place, marks it done, and reboots
|
|
# into the player. One file to distribute instead of four that must all land intact.
|
|
#
|
|
# ⚠️ The zip must expand to files AT ITS ROOT — no wrapper directory. A player extracts to the
|
|
# storage root, so a nested folder puts autorun.brs somewhere the player never looks and the
|
|
# card silently does nothing. That is why this zips from *inside* the staging directory.
|
|
#
|
|
# ⚠️ autorun.brs must NOT sit next to autorun.zip on the storage root: its presence stops the zip
|
|
# being processed at all. It belongs inside, which is where this puts it.
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")/.."
|
|
|
|
SERVER=""
|
|
OUT="brightsign/autorun.zip"
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--server) SERVER="${2:-}"; shift 2 ;;
|
|
-o|--out) OUT="${2:-}"; shift 2 ;;
|
|
-h|--help) sed -n '2,12p' "$0"; exit 0 ;;
|
|
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
command -v zip >/dev/null || { echo "ERROR: 'zip' is not installed." >&2; exit 1; }
|
|
|
|
STAGE="$(mktemp -d)"
|
|
trap 'rm -rf "$STAGE"' EXIT
|
|
|
|
# The payload. autozip.brs must be here too: it is what the NEXT player to receive this archive
|
|
# runs, and it has to survive being extracted alongside everything else.
|
|
cp brightsign/autozip.brs "$STAGE/"
|
|
cp brightsign/autorun.brs "$STAGE/"
|
|
cp brightsign/offline.html "$STAGE/"
|
|
cp brightsign/screentinker.json "$STAGE/"
|
|
|
|
# Stamp the version into the host so the script REPORTS the version it actually is. A package that
|
|
# ships reporting the old version is applied, reports the old version, and is offered again on the
|
|
# next check — forever. server/lib/brightsign-package.js does the identical substitution, anchored
|
|
# on the same ST_PACKAGE_VERSION marker, so a zip built here and one built by the server agree.
|
|
VERSION="$(cat VERSION 2>/dev/null | tr -d '[:space:]')"
|
|
if [ -n "$VERSION" ]; then
|
|
python3 - "$STAGE/autorun.brs" "$VERSION" <<'PY'
|
|
import re, sys
|
|
path, version = sys.argv[1], sys.argv[2]
|
|
src = open(path).read()
|
|
out = re.sub(r'return "[^"]*"(\s*\'\s*ST_PACKAGE_VERSION)', 'return "%s"\\1' % version, src)
|
|
if out == src:
|
|
sys.exit("ERROR: ST_PACKAGE_VERSION marker not found in autorun.brs — refusing to ship an "
|
|
"unstamped package, which would loop on every update check.")
|
|
open(path, 'w').write(out)
|
|
PY
|
|
echo " stamped package version $VERSION"
|
|
fi
|
|
|
|
# Point a batch at a specific server without hand-editing each card.
|
|
if [ -n "$SERVER" ]; then
|
|
python3 - "$STAGE/screentinker.json" "$SERVER" <<'PY'
|
|
import json, sys
|
|
path, server = sys.argv[1], sys.argv[2]
|
|
cfg = json.load(open(path))
|
|
cfg['server_url'] = server
|
|
json.dump(cfg, open(path, 'w'), indent=2)
|
|
PY
|
|
echo " server_url set to $SERVER"
|
|
fi
|
|
|
|
mkdir -p "$(dirname "$OUT")"
|
|
rm -f "$OUT"
|
|
ABS_OUT="$(cd "$(dirname "$OUT")" && pwd)/$(basename "$OUT")"
|
|
|
|
# -j would flatten any directories we add later; instead cd in and zip '.' so the archive root IS
|
|
# the staging root, and future subdirectories keep their structure.
|
|
( cd "$STAGE" && zip -q -r -X "$ABS_OUT" . )
|
|
|
|
echo " built $OUT"
|
|
unzip -l "$OUT" | sed 's/^/ /'
|
|
|
|
# Prove the root-level invariant rather than trusting it: this is the one mistake that makes a
|
|
# card look blank to the player, and it is invisible until hardware refuses to boot.
|
|
if unzip -l "$OUT" | awk 'NR>3 && $4 ~ /\// && $4 !~ /^[^\/]+$/ {print $4}' | grep -qE '^[^/]+/'; then
|
|
echo " NOTE: archive contains directories — verify they are intended subdirectories, not a wrapper."
|
|
fi
|
|
if ! unzip -l "$OUT" | grep -qE ' autorun\.brs$'; then
|
|
echo "ERROR: autorun.brs is not at the archive root — the player would never find it." >&2
|
|
exit 1
|
|
fi
|
|
if ! unzip -l "$OUT" | grep -qE ' autozip\.brs$'; then
|
|
echo "ERROR: autozip.brs is missing — nothing would unpack this archive." >&2
|
|
exit 1
|
|
fi
|
|
echo " root-level layout verified"
|