screentinker/scripts/build-server-boot-zip.sh
screentinker 0b063cd415
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Licence gate + SBOM (production deps) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
Make the on-device server opt-in, and stop the status port answering the LAN (#291)
A fleet gets one package, and exactly one box per site should host the server.
Defaulting to on would mean every player that ever received this package
started listening on 8181, and the mistake would stay invisible until two of
them fought over the same displays.

st-config.json on the storage root, {"server": 1}, switches it on. Absent,
unreadable, unparseable, or anything other than an affirmative value leaves it
off - there is no reading of a broken config file that should end with a
device deciding to host a server. It sits at the root rather than in data/
because that is where an operator drops it over the DWS, and autozip never
writes it, so a re-provision cannot silently flip a site either way. The
package ships st-config.example.json, never st-config.json, for the same
reason.

With the server off, NOTHING listens: roNodeJs is never created, so there is
no 8181 and no 8182. The page is told through its URL rather than discovering
it, because "nothing is answering" would otherwise render as a fault and send
someone looking for a server that was never meant to exist. It now has a
fourth state that says so and offers the one line of JSON that changes it.

Separately: the status listener was bound to every interface, so anything on
the customer's LAN could read the install log, disk usage, the device's own
address and a tail of the server's console - that last one carries whatever
the server printed most recently. Its only consumer is a page on the same
device. Now 127.0.0.1 only, confirmed against /proc/net/tcp rather than by
probing, after a first attempt at verifying it fell back to loopback and
reported that as the LAN result.


Claude-Session: https://claude.ai/code/session_014kfhrUPit5MCqxeTQyqr56

Co-authored-by: Dan Walters <dan.walters@bytetinker.net>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:32:37 -05:00

68 lines
3.2 KiB
Bash
Executable file

#!/bin/bash
# Build brightsign/autorun-boot.zip — ONLY the four files needed to start, no server payload.
#
# scripts/build-server-boot-zip.sh [-o path]
#
# WHY THIS EXISTS. The full server package is ~73MB across 9,356 entries, and BrightSignOS cannot
# open it: the boot-time autorun scan reports
#
# Failed to use zipped 'SSD:/autorun.zip': ZipArchive error at line 91
#
# and falls through to "Load or runtime error in autorun. Forcing recovery." Provisioning CAN unpack
# the same archive — the files land on disk — so the limit is specifically in the OS's own zip
# reader, not in the archive. Path lengths (max 182 chars) and depth (8) are well inside anything
# reasonable, which leaves size and entry count.
#
# So the OS gets an archive shaped exactly like the player package that already works on this
# hardware: a handful of small files, STORED, at the root. The ~71MB of server + node_modules is
# delivered separately and unpacked by Node, which has no such limit.
#
# This build is deliberately ALSO the isolation test: if the player boots this and shows
# "server payload not installed" on screen, the size hypothesis is confirmed and the two-stage
# design is right. If it still fails to open THIS, the problem is something else entirely and no
# amount of splitting would have helped.
set -euo pipefail
cd "$(dirname "$0")/.."
OUT="brightsign/autorun-boot.zip"
while [ $# -gt 0 ]; do
case "$1" in
-o|--out) OUT="${2:-}"; shift 2 ;;
-h|--help) sed -n '2,24p' "$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
cp brightsign/autozip.brs "$STAGE/autozip.brs"
cp brightsign/server/autorun.brs "$STAGE/autorun.brs"
cp brightsign/server/bs-server-boot.js "$STAGE/bs-server-boot.js"
cp brightsign/server/bs-payload-install.js "$STAGE/bs-payload-install.js"
cp brightsign/server/node-server.html "$STAGE/node-server.html"
cp brightsign/server/server.env.example "$STAGE/server.env.example"
# Shipped as .example ONLY. A file named st-config.json would be extracted over the operator's own
# on every re-provision, silently switching a site's server on or off.
cp brightsign/server/st-config.example.json "$STAGE/st-config.example.json"
mkdir -p "$(dirname "$OUT")"
rm -f "$OUT"
ABS_OUT="$(cd "$(dirname "$OUT")" && pwd)/$(basename "$OUT")"
# STORED, for the same reason as every other package here: roBrightPackage documents "no
# compression" as the universally safe option, and a deflated archive deploys perfectly then fails
# to open on the player.
( cd "$STAGE" && zip -q -r -X -0 "$ABS_OUT" . )
echo " built $OUT ($(du -h "$ABS_OUT" | cut -f1))"
unzip -l "$OUT" | sed 's/^/ /'
LISTING="$(unzip -l "$OUT")"
for required in autorun.brs autozip.brs bs-server-boot.js bs-payload-install.js node-server.html; do
case "$LISTING" in *" $required"*) ;; *) echo "ERROR: $required missing" >&2; exit 1 ;; esac
done
COMPRESSED="$(unzip -v "$OUT" | awk '$1 ~ /^[0-9]+$/ && $2 ~ /^[A-Za-z]/ && $2 != "Stored" {print $2}' | head -1)"
[ -n "$COMPRESSED" ] && { echo "ERROR: compressed members present" >&2; exit 1; }
echo " root-level layout verified, all members stored"