Files
forgefirm/scripts/install-forgefirm.sh
T
ScottW514 d482e76402 installer: a download that resumes and retries, and an install log
A field install failed on "firmware download failed", and worked after a
reboot. The download was one bare curl -fL: no retry, no resume, no bound
on a stalled transfer, and nothing on the machine recorded what had gone
wrong.

The download. download_fw makes up to five tries, 5, 15, 30 and 60 seconds
apart. Each try resumes the partial file (curl -C -) and is bounded: 20 s
to connect, and a transfer below 1 KB/s for 30 s ends the try. The file is
written as forgefirm.fw.part and takes its name only when curl finished;
the signature check that follows is what vouches for its content. A full
disk (curl 23) and a release that is not there (HTTP 404) end the tries at
once, because waiting cannot fix them. A partial file the server will not
resume (curl 33 or 36, HTTP 416) starts over. The loop is the installer's
own rather than curl --retry: the factory curl on the bench reference is
7.69.1, whose --retry does not count a resolver failure or a dropped
transfer as retryable, and older factory builds carry older curls. The
owner sees the reason in words with each retry, and the final failure says
that a re-run goes straight to the download, because the archives are kept.

The log. Every run appends to /data/log/forgefirm/install/install.log, in
the log tree's own line format (UTC, program "install"): the installer's
md5 (which revision ran), the factory version and the slots, the owner's
answers, each archive, each download try with curl's exit code, the HTTP
code and the reason, the machine's clock at each try (a wrong clock breaks
TLS), and after a failed try the address, the default route, the resolver
and whether github.com resolves; then the signature and identity checks,
the write, the boot selection, and the reason for any failure through
die(). The log is appended across runs, so the run that failed is still
there after the run that worked. Logging never fails the install.
forgectrl's log export carries the directory (forgectrl 0dae758).

Proven: tests/test_installer.py runs the installer's own functions under
sh against a scripted curl - a clean download, a resolver failure and a
dropped transfer that resume to the full file, the tries running out, 404
and a full disk ending them at once, a stale partial file starting over,
the TLS reason naming the clock, every log line in the tree format, die()
leaving its reason, and an unwritable log not failing the run. The whole
host suite, 409 tests, passes under Linux and the coverage lint is clean.
Bench: the same functions under the factory firmware's own shell (busybox
1.31.1 ash, the factory slot of the bench reference in a chroot) resumed,
retried, ran out of tries and logged exactly as under sh.

Acceptance: logs.tree-tail-export now plants a probe file in the install
directory and requires it back in the export bundle, its line intact and
its MAC and IPv4 address redacted, and requires an install log in the
bundle when the machine has one. The installer itself is not on the image:
the install page fetches it from master, so it is live with this push.
2026-09-19 16:20:32 -04:00

495 lines
21 KiB
Bash

#!/bin/sh
# Copyright 2020-2026 514 LLC d/b/a OpenGlow
# Written by Scott Wiederhold
# https://community.openglow.org
# SPDX-License-Identifier: MIT
#
# Single-stage OpenGlow/ForgeFIRM installer. Runs on FACTORY firmware:
# 1. archives the factory rootfs slots and the recovery boot partitions
# to /data/forgefirm/archive (offline factory restore, forever),
# 2. applies the signed ForgeFIRM .fw to the INACTIVE rootfs slot using
# the factory's own fwup (signature-verified),
# 3. flips the saved U-Boot environment to the new slot and reboots.
# The factory /data partition is never repartitioned or modified beyond
# the archive directory; the active factory slot stays bootable.
#
# Usage: install-forgefirm.sh [local-forgefirm.fw]
# With no argument the latest release .fw is downloaded from GitHub. The
# download resumes and retries; every run appends its steps to the
# install log in the ForgeFIRM log tree, which the panel's log export
# carries.
RELEASE_FW_URL="https://github.com/openglow-org/forgefirm/releases/latest/download/forgefirm.fw"
ARCHIVE_DIR="/data/forgefirm/archive"
FW_FILE="/data/forgefirm/forgefirm.fw"
MIN_DATA_FREE_KB=300000
LOG_DIR="/data/log/forgefirm/install"
LOG_FILE="$LOG_DIR/install.log"
DL_ATTEMPTS=5 # download tries before the install gives up
DL_DELAYS="5 15 30 60" # seconds to wait before tries 2, 3, 4, 5
# ForgeFIRM release-signing public key (raw 32-byte Ed25519, the format the
# factory's fwup 0.14.2 expects).
PUBKEY='\x7c\x7c\x3f\x37\x91\xff\xe6\xdb\x81\xc6\x34\x41\x4b\x6f\xab\xed\x14\x65\xfb\x29\x25\xb6\xb1\x63\xb2\x1d\x38\xcb\xfb\x85\x91\x75'
LIGHTRED="\033[1;31m"
YELLOW="\033[1;33m"
BRIGHT="\033[1;39m"
RESET="\033[0m"
ASTERISK="${LIGHTRED}✺${RESET}"
# log <SEVERITY> <message>: one line in the log tree's own format
# (timestamp, program[pid], severity, message). The log is appended
# across runs, so a run that failed is still on the machine after the run
# that worked. Logging never fails the install: with no writable log the
# lines go nowhere.
log () {
[ -n "$LOG_OK" ] || return 0
{ echo "$(date -u '+%Y-%m-%dT%H:%M:%S+00:00') install[$$] $1 $2" >> "$LOG_FILE"; } 2>/dev/null
return 0
}
die () {
log ERR "install failed: $1"
echo
echo -e "${LIGHTRED}!! INSTALL FAILED:${RESET} $1"
echo -e "${LIGHTRED}!! No boot change was made unless stated otherwise. Fix and re-run.${RESET}"
exit 1
}
stop_gf_services () {
echo -n -e "${ASTERISK}Stopping Glowforge services"
for SVC in glowforge glowforge-datalogger glowforge-updater bugeggs; do
sv stop /sv/$SVC 2>/dev/null >/dev/null; echo -n "."
sv stop /sv/$SVC/log 2>/dev/null >/dev/null; echo -n "."
done
echo "."
}
# archive_dev <src-device> <out.img.gz>: dd|gzip with a live progress
# line (compressed MB so far - old busybox dd has no status=progress).
# dd's exit status is captured via a file so a read failure is not
# masked by gzip succeeding on truncated input. The archive is written
# as <out>.part and renamed only when both ends succeeded: a run that
# dies mid-archive (power, a dropped SSH session) leaves nothing under
# the final name for a rerun to mistake for a complete archive.
archive_dev () {
RC_FILE=$(mktemp /tmp/ffinstall.rc.XXXXXX) || return 1
rm -f "$RC_FILE"
PART="$2.part"
rm -f "$PART"
( dd if="$1" bs=1M 2>/dev/null; echo $? > "$RC_FILE" ) | gzip -1 > "$PART" &
GZPID=$!
while kill -0 "$GZPID" 2>/dev/null; do
sleep 3
SZ=$(wc -c < "$PART" 2>/dev/null)
printf '\r %s MB compressed...' "$((${SZ:-0} / 1048576))"
done
wait "$GZPID"
GZRC=$?
DDRC=$(cat "$RC_FILE" 2>/dev/null)
rm -f "$RC_FILE"
printf '\r %s MB compressed. \n' "$(($(wc -c < "$PART") / 1048576))"
if [ "$GZRC" = "0" ] && [ "$DDRC" = "0" ]; then
mv "$PART" "$2"
else
rm -f "$PART"
return 1
fi
}
# archived_ok <out.img.gz>: an existing archive counts only when the
# manifest records it and the gzip stream is whole. Anything else (a file
# left by an older installer's interrupted run, a manifest line that
# never got written) is archived again.
archived_ok () {
[ -s "$1" ] || return 1
grep -q " $(basename "$1") md5=" "$ARCHIVE_DIR/manifest" 2>/dev/null || return 1
gzip -t "$1" 2>/dev/null
}
# slot_probe <1|2>: sets S_TYPE (factory|forgefirm|empty|unknown),
# S_VER (the build datetime, used for archive naming), and S_FWVER (the
# semantic FIRMWARE_VERSION, for display; factory only). The active slot
# is read from the running rootfs; others are mounted read-only under
# /tmp (newer factory firmware has no /factory/imgN mounts, and the
# rootfs is read-only). Sets S_TYPE=empty for a mountable-but-unknown
# filesystem and S_TYPE=unknown when the slot will not even mount.
slot_probe () {
S_TYPE=unknown; S_VER=""; S_FWVER=""; S_MOUNTED=""
if [ "$1" = "$ACTIVE" ]; then
RD=""
else
RD=$(sed -n "s|^/dev/mmcblk2p$1 \([^ ]*\).*|\1|p" /proc/mounts | head -n 1)
if [ -z "$RD" ]; then
S_MOUNTED=yes
RD=$(mktemp -d /tmp/ffinstall.probe.XXXXXX) || return 1
mount -o ro -t ext4 "/dev/mmcblk2p$1" "$RD" 2>/dev/null \
|| { rmdir "$RD" 2>/dev/null; S_TYPE=unknown; return 0; }
fi
fi
if [ -f "$RD/etc/forgefirm-version" ]; then
S_TYPE=forgefirm; S_VER=$(cat "$RD/etc/forgefirm-version")
elif [ -f "$RD/etc/version" ]; then
S_TYPE=factory; S_VER=$(cat "$RD/etc/version")
S_FWVER=$(sed -n 's/^FIRMWARE_VERSION[[:space:]]*=[[:space:]]*\([^[:space:]]*\).*/\1/p' \
"$RD/etc/build" 2>/dev/null)
else
S_TYPE=empty
fi
if [ -n "$S_MOUNTED" ]; then
umount "$RD" 2>/dev/null
rmdir "$RD" 2>/dev/null
fi
}
# Human label for the last slot_probe result.
slot_desc () {
case "$S_TYPE" in
factory) [ -n "$S_FWVER" ] && echo "factory firmware v$S_FWVER" \
|| echo "factory firmware $S_VER" ;;
forgefirm) echo "ForgeFIRM $S_VER" ;;
empty) echo "an unrecognized filesystem" ;;
*) echo "unknown/unreadable content" ;;
esac
}
# ver_lt A B: true when semantic version A < B (leading v ignored).
# Returns false on any non-numeric component (e.g. a dev datetime
# stamp) - no verdict means no downgrade prompt, never a refusal.
ver_lt () {
VA=${1#v}; VB=${2#v}
[ "$VA" = "$VB" ] && return 1
VI=1
while [ "$VI" -le 3 ]; do
A=$(echo "$VA" | cut -d. -f$VI)
B=$(echo "$VB" | cut -d. -f$VI)
A=${A:-0}; B=${B:-0}
case "$A$B" in *[!0-9]*) return 1 ;; esac
[ "$A" -lt "$B" ] && return 0
[ "$A" -gt "$B" ] && return 1
VI=$((VI + 1))
done
return 1
}
# Verified atomic env flip (all four variables, classic u-boot-tools script
# format first - that is what factory firmware ships - then libubootenv
# format, then per-variable writes; read-back verified in every case).
# Config selection matches ffboot: newer factory firmware's generic
# fw_env.config points at the wrong device; its per-device
# fw_env_mmcblk2.config is the correct one for the eMMC environment.
if [ -f "/etc/fw_env_mmcblk2.config" ] && [ ! -d "/factory" ]; then
FWCONFIG="/etc/fw_env_mmcblk2.config"
else
FWCONFIG="/etc/fw_env.config"
fi
env_get () { fw_printenv -c "$FWCONFIG" -n "$1" 2>/dev/null; }
env_verify () {
[ "$(env_get mmcdev)" = "$1" ] && [ "$(env_get mmchwpart)" = "$2" ] && \
[ "$(env_get mmcpart)" = "$3" ] && [ "$(env_get mmcroot)" = "$4" ]
}
set_env () {
SCRIPT=$(mktemp /tmp/ffinstall.env.XXXXXX) || return 1
printf 'mmcdev %s\nmmchwpart %s\nmmcpart %s\nmmcroot %s\n' "$1" "$2" "$3" "$4" > "$SCRIPT"
fw_setenv -c "$FWCONFIG" -s "$SCRIPT" 2>/dev/null
if env_verify "$1" "$2" "$3" "$4"; then rm -f "$SCRIPT"; return 0; fi
printf 'mmcdev=%s\nmmchwpart=%s\nmmcpart=%s\nmmcroot=%s\n' "$1" "$2" "$3" "$4" > "$SCRIPT"
fw_setenv -c "$FWCONFIG" -s "$SCRIPT" 2>/dev/null
rm -f "$SCRIPT"
if env_verify "$1" "$2" "$3" "$4"; then return 0; fi
fw_setenv -c "$FWCONFIG" mmcdev "$1" && \
fw_setenv -c "$FWCONFIG" mmchwpart "$2" && \
fw_setenv -c "$FWCONFIG" mmcpart "$3" && \
fw_setenv -c "$FWCONFIG" mmcroot "$4"
env_verify "$1" "$2" "$3" "$4"
}
# curl_why <exit-code>: why a download attempt failed, in words.
curl_why () {
case "$1" in
5|6) echo "the host name did not resolve (DNS)" ;;
7) echo "the connection was refused or unreachable" ;;
18|52|55|56) echo "the connection dropped mid-transfer" ;;
22) echo "the server answered HTTP $2" ;;
23) echo "the file could not be written" ;;
28) echo "the connection timed out or stalled" ;;
35|51|60) echo "the TLS handshake failed (a wrong clock does this: $(date -u '+%Y-%m-%d %H:%M') UTC)" ;;
*) echo "curl error $1" ;;
esac
}
# net_snapshot: what the network looked like when an attempt failed - the
# address, the default route, the resolver, whether the release host
# resolves - into the install log only.
net_snapshot () {
log INFO "net: wlan0 $(ip -4 addr show dev wlan0 2>/dev/null | sed -n 's/^ *inet \([^ ]*\).*/\1/p' | head -n 1)"
log INFO "net: route $(ip route 2>/dev/null | grep '^default' | head -n 1)"
log INFO "net: resolver $(grep '^nameserver' /etc/resolv.conf 2>/dev/null | tr '\n' ' ')"
if nslookup github.com >/dev/null 2>&1; then
log INFO "net: github.com resolves"
else
log WARNING "net: github.com does not resolve"
fi
}
# download_fw: fetch the release into $FW_FILE. A home network drops, a
# resolver hiccups, a transfer stalls: each try resumes the partial file
# where the last one stopped, and the tries are spaced out. The file
# lands as <file>.part and takes its name only when curl finished, and
# the signature check that follows is what vouches for its content. Two
# failures end the tries at once, because waiting cannot fix them: a full
# disk, and a release that is not there (HTTP 404).
download_fw () {
PART="$FW_FILE.part"
rm -f "$PART"
TRY=1
set -- $DL_DELAYS
while :; do
log INFO "download: try $TRY of $DL_ATTEMPTS, clock $(date -u '+%Y-%m-%d %H:%M:%S') UTC"
HTTP=$(curl -fL -C - --connect-timeout 20 --speed-limit 1024 --speed-time 30 \
-w '%{http_code}' --output "$PART" "$RELEASE_FW_URL")
RC=$?
if [ "$RC" = "0" ]; then
mv "$PART" "$FW_FILE" || return 1
log INFO "download: complete on try $TRY, $(wc -c < "$FW_FILE") bytes"
return 0
fi
WHY=$(curl_why "$RC" "$HTTP")
SOFAR=0
[ -f "$PART" ] && SOFAR=$(wc -c < "$PART")
log WARNING "download: try $TRY failed: curl exit $RC, HTTP ${HTTP:-none}: $WHY ($SOFAR bytes so far)"
net_snapshot
# A partial file the server will not resume (no range support, or a
# range past its end) starts over rather than failing every try.
case "$RC:$HTTP" in 33:*|36:*|22:416) rm -f "$PART" ;; esac
if [ "$RC" = "23" ] || [ "$RC:$HTTP" = "22:404" ] || [ "$TRY" -ge "$DL_ATTEMPTS" ]; then
rm -f "$PART"
DL_WHY="$WHY"
return 1
fi
WAIT=${1:-60}; [ $# -gt 0 ] && shift
echo -e "${YELLOW}!! Download failed: $WHY.${RESET} Trying again in ${WAIT}s (try $((TRY + 1)) of $DL_ATTEMPTS)..."
sleep "$WAIT"
TRY=$((TRY + 1))
done
}
# The install log, before anything can fail.
mkdir -p "$LOG_DIR" 2>/dev/null && : >> "$LOG_FILE" 2>/dev/null && LOG_OK=yes
INSTALLER_MD5=""
[ -f "$0" ] && INSTALLER_MD5=$(md5sum "$0" 2>/dev/null | cut -d' ' -f1)
log INFO "run start: installer md5=${INSTALLER_MD5:-unknown}, firmware source: ${1:-latest release}"
echo
echo -e "${LIGHTRED} ✺┈┈┈┈┈┈${RESET}"
echo -e "${BRIGHT}Open${RESET}Glow ForgeFIRM Installation Tool"
echo
# --- pre-flight ---------------------------------------------------------------
[ -f /etc/version ] && [ -d /glowforge ] \
|| die "this script must be run from the FACTORY firmware"
command -v fwup >/dev/null || die "fwup not found on this system"
command -v fw_setenv >/dev/null || die "fw_setenv not found on this system"
[ -f "$FWCONFIG" ] || die "$FWCONFIG not found"
BOOTED=$(sed -n 's/.*root=\([^ ]*\).*/\1/p' /proc/cmdline)
case "$BOOTED" in
/dev/mmcblk2p1) ACTIVE=1; TARGET=2; TASK=upgrade.b ;;
/dev/mmcblk2p2) ACTIVE=2; TARGET=1; TASK=upgrade.a ;;
*) die "booted from $BOOTED - expected factory eMMC slot 1 or 2" ;;
esac
for N in 1 2; do
SZ=$(cat /sys/class/block/mmcblk2p$N/size 2>/dev/null)
[ "$SZ" = "409600" ] || die "slot $N is not the 200 MiB factory layout (size=$SZ)"
done
FREE_KB=$(df -k /data | tail -1 | awk '{print $4}')
[ "$FREE_KB" -ge "$MIN_DATA_FREE_KB" ] 2>/dev/null \
|| die "need ${MIN_DATA_FREE_KB} KB free on /data, have ${FREE_KB:-unknown}"
log INFO "pre-flight: factory $(cat /etc/version), booted slot $ACTIVE, target slot $TARGET, ${FREE_KB} KB free on /data, $(curl -V 2>/dev/null | head -n 1)"
echo -e "${LIGHTRED}!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!${RESET}"
echo -e "${YELLOW} THIS IS EXPERIMENTAL SOFTWARE!${RESET}"
echo -e "The installation and/or use of this software may"
echo -e "result in damage to your device and/or property,"
echo -e "loss of warranty, and severe bodily injury and/or"
echo -e "death. This software is not affiliated with or"
echo -e "endorsed by Glowforge. ${BRIGHT}USE IT AT YOUR OWN RISK!${RESET}"
echo -e "${LIGHTRED}!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!${RESET}"
echo
echo -e "Booted slot: $ACTIVE - ForgeFIRM will be installed to slot $TARGET."
echo -e "The factory firmware in slot $ACTIVE stays installed and bootable."
echo
# What is in the target slot, and will it be archived first? The archive
# step below backs up factory images; anything else in the target slot
# is overwritten without a backup, so make that explicit.
slot_probe "$TARGET"
TARGET_DESC=$(slot_desc)
echo -e "Slot $TARGET currently holds: ${BRIGHT}$TARGET_DESC${RESET}"
log INFO "slot $TARGET holds: $TARGET_DESC"
if [ "$S_TYPE" = "factory" ]; then
echo -e "It will be archived to /data before being overwritten."
else
echo -e "${YELLOW}!! This is NOT factory firmware and will NOT be archived.${RESET}"
echo -e "${YELLOW}!! Its contents will be permanently destroyed.${RESET}"
echo
read -p "Type ERASE to overwrite slot $TARGET, or anything else to abort: " erase
echo
if [ "$erase" != "ERASE" ]; then
log NOTICE "operator declined to erase slot $TARGET; no changes made"
echo "Aborting without changes."
exit 0
fi
fi
echo
read -p "Are you sure you want to continue [N/y]? " continue
echo
if [ "$continue" != "y" ]; then
log NOTICE "operator declined to continue; no changes made"
echo "Wise choice. Exiting without changes."
exit 0
fi
log INFO "operator confirmed; stopping the Glowforge services"
stop_gf_services
# --- archive factory content --------------------------------------------------
mkdir -p "$ARCHIVE_DIR"
for N in 1 2; do
slot_probe $N
[ "$S_TYPE" = "factory" ] || continue
ARC="$ARCHIVE_DIR/factory-rootfs-$S_VER.img.gz"
if archived_ok "$ARC"; then
echo -e "${ASTERISK}Slot $N (factory $S_VER) already archived."
log INFO "archive: slot $N (factory $S_VER) already archived"
continue
fi
[ -e "$ARC" ] && echo -e "${ASTERISK}Slot $N: the archive on disk is incomplete; archiving again."
echo -e "${ASTERISK}Archiving slot $N ($(slot_desc)) - takes a few minutes:"
archive_dev /dev/mmcblk2p$N "$ARC" \
|| { rm -f "$ARC"; die "archiving slot $N failed"; }
echo "$(date '+%Y-%m-%d %H:%M:%S') slot$N factory $S_VER ver=${S_FWVER:-unknown} $(basename $ARC) md5=$(md5sum "$ARC" | cut -d' ' -f1)" >> "$ARCHIVE_DIR/manifest"
log INFO "archive: slot $N (factory $S_VER) -> $(basename "$ARC"), $(wc -c < "$ARC") bytes"
done
for B in 0 1; do
ARC="$ARCHIVE_DIR/recovery-boot$B.img.gz"
archived_ok "$ARC" && continue
echo -e "${ASTERISK}Archiving recovery boot$B:"
archive_dev /dev/mmcblk2boot$B "$ARC" \
|| { rm -f "$ARC"; die "archiving boot$B failed"; }
echo "$(date '+%Y-%m-%d %H:%M:%S') boot$B recovery - $(basename $ARC) md5=$(md5sum "$ARC" | cut -d' ' -f1)" >> "$ARCHIVE_DIR/manifest"
log INFO "archive: boot$B -> $(basename "$ARC"), $(wc -c < "$ARC") bytes"
done
# --- acquire the ForgeFIRM .fw ------------------------------------------------
mkdir -p /data/forgefirm
if [ -n "$1" ]; then
[ -s "$1" ] || die "local firmware file '$1' not found"
cp "$1" "$FW_FILE" || die "cannot copy '$1' to $FW_FILE"
echo -e "${ASTERISK}Using local firmware file: $1"
log INFO "firmware: local file $1, $(wc -c < "$FW_FILE") bytes"
else
echo -e "${ASTERISK}Downloading latest OpenGlow/ForgeFIRM release:"
download_fw \
|| die "firmware download failed: ${DL_WHY:-unknown}. Check the network and re-run; the archives are kept, so a re-run goes straight to the download"
fi
# --- verify signature ---------------------------------------------------------
KEYFILE=$(mktemp /tmp/forgefirm.pub.XXXXXX) || die "cannot create temp file"
printf "$PUBKEY" > "$KEYFILE"
[ "$(wc -c < "$KEYFILE")" = "32" ] || die "embedded public key corrupt"
echo -e "${ASTERISK}Verifying firmware signature:"
fwup -V -i "$FW_FILE" -p "$KEYFILE" || { rm -f "$KEYFILE"; die "signature verification FAILED - refusing to install"; }
# --- archive identity + downgrade gate ----------------------------------------
META=$(fwup -m -i "$FW_FILE")
M_PRODUCT=$(echo "$META" | sed -n 's/^meta-product="\(.*\)"$/\1/p')
M_PLATFORM=$(echo "$META" | sed -n 's/^meta-platform="\(.*\)"$/\1/p')
M_VERSION=$(echo "$META" | sed -n 's/^meta-version="\(.*\)"$/\1/p')
[ "$M_PRODUCT" = "ForgeFIRM firmware" ] \
|| { rm -f "$KEYFILE"; die "archive product is '$M_PRODUCT', not ForgeFIRM firmware - wrong archive"; }
[ "$M_PLATFORM" = "glowforge" ] \
|| { rm -f "$KEYFILE"; die "archive platform is '$M_PLATFORM', not glowforge - wrong archive"; }
echo -e "${ASTERISK}Archive: $M_PRODUCT $M_VERSION ($M_PLATFORM)"
log INFO "firmware: signature verified; $M_PRODUCT $M_VERSION ($M_PLATFORM)"
# A validly signed OLDER release must never install silently; downgrades
# need an explicit yes (rollback stays possible, just deliberate).
INSTALLED=""
for S in 1 2; do
slot_probe "$S"
if [ "$S_TYPE" = "forgefirm" ] && [ -n "$S_VER" ]; then
if [ -z "$INSTALLED" ] || ver_lt "$INSTALLED" "$S_VER"; then
INSTALLED="$S_VER"
fi
fi
done
if [ -n "$INSTALLED" ] && ver_lt "$M_VERSION" "$INSTALLED"; then
echo -e "${ASTERISK}This archive ($M_VERSION) is OLDER than the installed ForgeFIRM ($INSTALLED)."
read -n1 -p "Install the downgrade anyway? [y/N] " YN
echo
case "$YN" in
y|Y) log NOTICE "operator accepted the downgrade from $INSTALLED to $M_VERSION" ;;
*) rm -f "$KEYFILE"; die "downgrade declined" ;;
esac
fi
# --- apply to the inactive slot -----------------------------------------------
echo -e "${ASTERISK}Writing ForgeFIRM to slot $TARGET (/dev/mmcblk2p$TARGET):"
log INFO "apply: writing $M_VERSION to slot $TARGET"
for M in $(sed -n "s|^/dev/mmcblk2p$TARGET \([^ ]*\).*|\1|p" /proc/mounts); do
umount "$M" 2>/dev/null
done
fwup -a -d "/dev/mmcblk2p$TARGET" -i "$FW_FILE" -t "$TASK" -p "$KEYFILE" \
|| { rm -f "$KEYFILE"; die "fwup apply failed - slot $TARGET is now undefined, factory slot $ACTIVE is untouched"; }
rm -f "$KEYFILE"
# --- post-write verify --------------------------------------------------------
MP=$(mktemp -d /tmp/ffinstall.verify.XXXXXX) || die "cannot create temp dir"
mount -o ro -t ext4 "/dev/mmcblk2p$TARGET" "$MP" || die "new rootfs does not mount"
NEWVER=$(cat "$MP/etc/forgefirm-version" 2>/dev/null)
[ -n "$NEWVER" ] || { umount "$MP"; die "new rootfs has no ForgeFIRM version stamp"; }
[ -f "$MP/boot/zImage" ] || { umount "$MP"; die "new rootfs has no kernel"; }
# Take ffboot (the factory-side boot-slot tool) from the rootfs we just
# signature-verified and mounted read-only - never fetch+exec it from a
# mutable network ref, which would be an unverified code path in an
# otherwise signature-gated install.
[ -s "$MP/usr/sbin/ffboot" ] \
|| { umount "$MP"; die "new rootfs does not contain /usr/sbin/ffboot"; }
cp "$MP/usr/sbin/ffboot" /data/ffboot.new \
|| { umount "$MP"; die "cannot copy ffboot out of the new rootfs"; }
umount "$MP"
rmdir "$MP" 2>/dev/null
echo -e "${ASTERISK}Slot $TARGET now holds ForgeFIRM $NEWVER"
log INFO "apply: slot $TARGET holds ForgeFIRM $NEWVER; the written filesystem verified"
# --- ffboot for the factory side ----------------------------------------------
mv /data/ffboot.new /data/ffboot
chmod +x /data/ffboot
# --- flip the boot selection --------------------------------------------------
echo -e "${ASTERISK}Setting boot to /dev/mmcblk2p$TARGET"
set_env 1 0 "$TARGET" "/dev/mmcblk2p$TARGET" \
|| die "environment write did not verify - boot selection unchanged; run /data/ffboot -e$TARGET manually"
log INFO "boot selection set to slot $TARGET and read back; install complete"
echo
echo -e "${BRIGHT}Installation complete.${RESET}"
echo -e "To return to factory firmware later: ${BRIGHT}/data/ffboot -e${RESET} (from ForgeFIRM: ${BRIGHT}ffboot -e${RESET})"
echo
read -n1 -p "Press any key to reboot into OpenGlow/ForgeFIRM..." continue
echo
log INFO "rebooting into ForgeFIRM $NEWVER"
sync
reboot
exit 0