screentinker/brightsign/autozip.brs
ScreenTinker 647a2bcc56 BrightSign: replace the Roku APIs, and a literal that stopped the script loading
The host scripts were written against the wrong reference. BrightScript is
Roku's language, the two API references read almost identically, and nothing
here can run either — so a call to an object that does not exist looked exactly
like a call to one that does. Verified on an XT245 and against BrightSign's
published reference; every item below was confirmed, not guessed.

THE ONE THAT COST A BOOT. `body$ = "{""width"":"` is not an escaped quote —
BrightScript has no escape sequences, so that is three adjacent literals with no
operator, and the compiler rejects the WHOLE FILE:

    ScriptLoadError: Syntax Error. (compile error &h02) in SSD:/autorun.brs(196)

Not a broken feature — no player at all, on a display showing nothing. Built
with Chr(34) now.

THE ONE IN THE FIELD. MatchFiles takes a DIRECTORY plus a pattern and returns
nothing when the pattern contains a separator; we passed a full path as both
arguments. FileExists() could never return true, for any file, on any player.
That is exactly what a consultant hit: "[st-autozip] no autorun.zip on any
volume" printed while `dir SD:` listed autorun.zip. It also silently disabled
the entire self-update path. (Related: `autorun.zip_invalid` on his card is not
an accusation — it is the rename BrightSign's own example performs AFTER a
successful unpack. Our STORED-only insistence fixed a problem that was never
there; deflate32 is supported.)

Roku objects that do not exist here, each of which disabled a feature quietly:
roFileSystem (~20 sites — the update path could never mark a package applied),
roMessageDigest (verification returned false unconditionally and burned an
attempt counter), PostFromStringWithRetry (a snapshot request raised "member
function not found" from inside the event loop and took the player down).
Replaced with MoveFile/DeleteFile, roHashGenerator, and an async POST on a
message port, which is the only documented way to read a POST body.

Unpack() returns Void, so `if not package.Unpack(...)` was a type error dressed
as an error check; success is now proven by looking for the extracted file. And
Unpack() DELETES everything already in its target — unpacking an update to the
volume root would have erased the player's provisioning and its whole content
pool as a side effect of a routine upgrade. It stages to a directory of its own
and moves files into place, deliberately never overwriting screentinker.json.

Also: SetMode() takes one argument (rotation belongs to SetScreenModes, which
REBOOTS, so it only fires on a real change); GetStorageStatus is unreliable with
"USBn:"; a load-error names its resource in `uri`, not `url`.

server/test/brightscript-api-surface.test.js is the cheap thing that would have
caught all of it: a deny-list of Roku APIs plus the argument shapes and literal
forms that compile and then do nothing. It cannot prove the scripts are right;
it stops these specific mistakes coming back. It has already earned its keep —
it caught a comment I had broken while writing this change.

Verified on hardware: the player loads clean from the NVMe, restores its cached
playlist and plays BEFORE the server connects, fetches media with the new
?rev= revision, and registers against alpha rc4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
2026-08-05 22:57:36 -05:00

111 lines
5 KiB
Plaintext

' ScreenTinker — autorun.zip unpacker.
'
' Ships INSIDE autorun.zip, at its root. The card (or internal flash) carries a single file —
' autorun.zip — and this script unpacks it in place, marks it done so it never re-extracts, and
' reboots into the real host.
'
' That is the whole point of the zip: one file to hand someone, or to drop on a hundred cards,
' instead of four files that must all arrive intact and in the right place. A partially-copied
' set of loose files boots into something broken; a partially-copied zip simply fails to extract
' and leaves the player where it was.
'
' ⚠️ autorun.brs must NOT sit next to autorun.zip on the storage root — its presence stops the zip
' being processed at all. autorun.brs belongs INSIDE the zip, which is where the build script puts
' it (scripts/build-autorun-zip.sh).
'
' Unpacks with roBrightPackage, which is what BrightSign's own tooling uses.
'
' On compression: roBrightPackage supports deflate32 with default options, PPMd, and "no
' compression". What it does NOT support is bzip2, LZMA, Deflate64 and Zip64 (the last being what
' Windows Explorer's built-in zipper produces). We build STORED, which is stricter than required and
' costs nothing at this size — but note that compression was NOT the cause of the deployment failure
' this script was blamed for. That was the MatchFiles bug below.
'
' Requires BrightSignOS 7.0.60+.
' WHERE the archive is. A player may be fed from USB, a card, an SSD, or internal flash — and the
' unit that drove this port boots from FLASH because its card interface is physically dead. Probing
' for the file beats assuming a volume: extracting to "SD:/" on a player with no card writes to a
' volume that does not exist, and the deployment silently does nothing.
Function SourceRoot() As String
volumes = ["USB1:", "SD:", "SD2:", "SSD:", "FLASH:"]
for each v in volumes
if FileExists(v + "/", "autorun.zip") then return v
end for
return ""
End Function
Sub Main()
root$ = SourceRoot()
if root$ = "" then
print "[st-autozip] no autorun.zip on any volume — nothing to do"
return
end if
zipPath$ = root$ + "/autorun.zip"
extractPath$ = root$ + "/"
donePath$ = root$ + "/autorun.zip.done"
print "[st-autozip] volume "; root$
if not FileExists(extractPath$, "autorun.zip") then
print "[st-autozip] no autorun.zip at "; zipPath$; " — nothing to do"
return
end if
' Idempotence. Without this the player extracts, reboots, extracts again, reboots again —
' a boot loop that looks like a hardware fault.
if FileExists(extractPath$, "autorun.zip.done") then
print "[st-autozip] already unpacked (autorun.zip.done present) — leaving it alone"
return
end if
print "[st-autozip] unpacking "; zipPath$
package = CreateObject("roBrightPackage", zipPath$)
if package = invalid then
print "[st-autozip] ERROR: could not open the archive — is it STORED (no compression)?"
' Deliberately NOT marking it done: a corrupt, truncated or wrongly-compressed copy should
' be retried once someone replaces the file, not silently skipped forever.
return
end if
' Unpack() returns VOID — there is no boolean to test, and `if not package.Unpack(...)` was a
' type error rather than an error check. Success is proven the only way that actually means
' anything: the file we came here to install is now on the card.
package.Unpack(extractPath$)
if not FileExists(extractPath$, "autorun.brs") then
print "[st-autozip] ERROR: unpack produced no autorun.brs — leaving the archive for a retry"
return
end if
print "[st-autozip] extracted"
' MoveFile/DeleteFile are GLOBAL functions on BrightSign. roFileSystem is a Roku object and does
' not exist here, so every one of these calls used to return invalid — which meant the archive
' was never marked done and the player never rebooted into the player it had just installed.
if not MoveFile(zipPath$, donePath$) then
print "[st-autozip] ERROR: could not rename the archive; refusing to reboot into a loop"
return
end if
print "[st-autozip] rebooting into the unpacked player"
sleep(2000)
RebootSystem()
End Sub
' Does [name] exist in directory [dir]?
'
' THE BUG THIS FIXES. The previous version passed a full path as BOTH arguments of MatchFiles.
' MatchFiles takes a DIRECTORY plus a pattern, and the documentation is explicit: "you will get no
' results if the pattern contains a directory separator". So it returned an empty list every time,
' for every file, on every player — and this script reported "no autorun.zip on any volume" while a
' `dir SD:` sat there listing autorun.zip. Reported from a real deployment on an HD1026.
'
' Two arguments rather than one path, so there is no string-splitting to get wrong.
Function FileExists(dir As String, name As String) As Boolean
files = MatchFiles(dir, name)
if files = invalid then return false
return files.Count() > 0
End Function