From ec340847f2ebfcfdc6b5049c560bb42544a033c5 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 23 Aug 2026 20:39:22 +0200 Subject: [PATCH] fix(install): preflight disk-space check before replacing the live binary The gzip fix in the previous commit only helps once a backup is being made; it doesn't address the actual moment that broke on real hardware: the cross-device mv/copy of the new binary into place ran out of space mid-write, leaving a truncated, non-executable binary as the live one. UBIFS is a log-structured flash filesystem, so space "freed" by overwriting the old binary isn't guaranteed reusable in time for the new one to land -- this happened on a device with 15.7MB available against a ~14.8MB binary. Add a preflight check before downloading anything: fetch the new binary's real size via a HEAD request (adapts automatically as binaries grow, instead of a threshold that goes stale every release) and compare against available space plus a flat 5MB safety margin. - Comfortably enough room for old + new + a compressed backup: proceed exactly as before, silently. - Enough for old + new but not enough extra for a backup: warn interactively and require explicit confirmation before proceeding without one. Reads from /dev/tty since the script is normally piped via `curl | sh` (stdin is consumed by the script itself). Defaults to the safe choice (abort) on empty input, matching the [y/N] prompt. AFTERTOUCH_FORCE_NO_BACKUP=yes overrides for non-interactive/scripted use. - Not enough room even for the replace itself: abort before starting the download, rather than attempting a doomed download/replace that could leave a truncated live binary. - No TTY available and the operator didn't set the override: abort rather than silently guessing. - HEAD request fails for any reason: skip the check with a warning rather than blocking the install on it. Verified: all five decision branches (plenty of room, warn+decline, warn+confirm, warn+forced-override, hard abort) produce the correct result under both dash and a real BusyBox v1.38.0 container, including the gzip/gunzip streaming backup and glob-based pruning from the previous commit. The HEAD-request size lookup was separately verified against a live GitHub release URL with real curl -- catching and fixing a bug where naively taking the first "content-length:" header grabbed the 302 redirect's (0), not the actual asset's, size. Not yet re-tested end-to-end on real hardware. Co-Authored-By: Claude Sonnet 5 --- scripts/on-device-install/install.sh | 85 +++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/scripts/on-device-install/install.sh b/scripts/on-device-install/install.sh index 774ad2cd..f2a7e0d3 100644 --- a/scripts/on-device-install/install.sh +++ b/scripts/on-device-install/install.sh @@ -106,6 +106,86 @@ for f in "$INSTALL_DIR/aftertouch-service".*.backup \ done echo "Disk usage after pre-install GC:"; df -h "$INSTALL_DIR" +# --- Preflight disk-space check ------------------------------------------ +# /mnt/nv is small (tens of MB) and binaries keep growing (Go 1.27 alone +# added ~640KB to this binary via its own new stdlib defaults, unrelated to +# this project's code). A prior attempt on real hardware ran out of space +# mid-replace and left a truncated, non-executable binary in place: UBIFS is +# a log-structured flash filesystem, so space freed by overwriting the old +# binary isn't necessarily reusable by the time the new one needs to land. +# Check upfront, with a safety margin, instead of discovering this mid-write. +# +# The new binary's size comes from a HEAD request rather than a hardcoded +# threshold, so this doesn't go stale as binaries grow across releases. +NEW_BINARY_BYTES=$(curl -sSLI --fail "$BINARY_URL" 2>/dev/null \ + | tr -d '\r' \ + | awk 'tolower($1) == "content-length:" {v=$2} END {print v}') || true + +AVAILABLE_KB=$(df -Pk "$INSTALL_DIR" | awk 'NR==2 {print $4}') + +CURRENT_BINARY_KB=0 +if [ -f "$INSTALL_DIR/aftertouch-service" ]; then + CURRENT_BINARY_KB=$(du -k "$INSTALL_DIR/aftertouch-service" | awk '{print $1}') +fi + +# Flat margin, not a percentage: covers UBIFS's own reserved/GC headroom on +# this log-structured flash filesystem plus general slack. +SAFETY_MARGIN_KB=5120 # 5 MB + +SKIP_BACKUP=no + +if [ -n "$NEW_BINARY_BYTES" ]; then + NEW_BINARY_KB=$((NEW_BINARY_BYTES / 1024)) + # Backups compress to roughly 70% of the original size in practice + # (observed: a ~14.8MB binary gzipped to ~10.1MB); used as a conservative + # estimate since the real ratio isn't known until compression actually runs. + BACKUP_ESTIMATE_KB=$((CURRENT_BINARY_KB * 7 / 10)) + + NEEDED_WITH_BACKUP_KB=$((NEW_BINARY_KB + BACKUP_ESTIMATE_KB + SAFETY_MARGIN_KB)) + NEEDED_NO_BACKUP_KB=$((NEW_BINARY_KB + SAFETY_MARGIN_KB)) + + if [ "$AVAILABLE_KB" -ge "$NEEDED_WITH_BACKUP_KB" ]; then + : # plenty of room; proceed normally, with a backup + elif [ "$AVAILABLE_KB" -ge "$NEEDED_NO_BACKUP_KB" ]; then + echo "WARNING: not enough free space on $INSTALL_DIR to keep a rollback" >&2 + echo "backup this time (${AVAILABLE_KB}KB available; ~${NEEDED_WITH_BACKUP_KB}KB" >&2 + echo "wanted with a backup, ~${NEEDED_NO_BACKUP_KB}KB without one)." >&2 + echo "Continuing will replace the current binary with NO way to" >&2 + echo "automatically undo it if something goes wrong." >&2 + if [ -n "${AFTERTOUCH_FORCE_NO_BACKUP:-}" ]; then + echo "Proceeding without a backup (AFTERTOUCH_FORCE_NO_BACKUP is set)." >&2 + SKIP_BACKUP=yes + elif [ -r /dev/tty ] && [ -w /dev/tty ]; then + printf 'Continue without a backup? [y/N] ' > /dev/tty + REPLY="" + read -r REPLY < /dev/tty || true + case "$REPLY" in + [Yy]*) SKIP_BACKUP=yes ;; + *) + echo "Aborting: refusing to proceed without a backup. Free up space" >&2 + echo "on $INSTALL_DIR and try again, or set AFTERTOUCH_FORCE_NO_BACKUP=yes" >&2 + echo "to proceed without one non-interactively." >&2 + exit 1 + ;; + esac + else + echo "No interactive terminal available to confirm; aborting." >&2 + echo "Set AFTERTOUCH_FORCE_NO_BACKUP=yes to proceed without a backup" >&2 + echo "non-interactively." >&2 + exit 1 + fi + else + echo "ERROR: not enough free space on $INSTALL_DIR to install AfterTouch" >&2 + echo "$VERSION safely (${AVAILABLE_KB}KB available, ~${NEEDED_NO_BACKUP_KB}KB" >&2 + echo "needed). Free up space and try again." >&2 + exit 1 + fi +else + echo "WARNING: could not determine the new binary's size ahead of time" >&2 + echo "(HEAD request to $BINARY_URL failed); skipping the preflight" >&2 + echo "disk-space check." >&2 +fi + curl \ -sSL \ -o "$UPDATE_TMP_DIR/binary" \ @@ -115,8 +195,11 @@ curl \ # Back up the current binary before overwriting so a one-step rollback # is always available. The version string comes from the binary itself; # if it is absent (very old build or corrupted) we fall back to a timestamp. +# Skipped entirely when the preflight check above decided (with the +# operator's explicit confirmation, or AFTERTOUCH_FORCE_NO_BACKUP) that +# there isn't room for one. BACKUP_FILE="" -if [ -f "$INSTALL_DIR/aftertouch-service" ]; then +if [ -f "$INSTALL_DIR/aftertouch-service" ] && [ "$SKIP_BACKUP" != "yes" ]; then current_version=$("$INSTALL_DIR/aftertouch-service" --version 2>/dev/null \ | awk '{print $NF}') || true if [ -z "$current_version" ] || [ "$current_version" = "dev" ]; then