diff --git a/tools/cutover.sh b/tools/cutover.sh new file mode 100755 index 0000000..d099890 --- /dev/null +++ b/tools/cutover.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# The one-time coordinated cutover (docs/cutover-proposal-final.md). Run only on Paul's explicit go, with the TV idle and +# wireless debugging on. +# tools/cutover.sh dry-run snapshot, checksums, restore rehearsal and the full computation; writes nothing new +# tools/cutover.sh execute I-HAVE-PAULS-GO the same, then writes the new schedules, checks them and starts Orbit +# tools/cutover.sh restore immediate rollback: the snapshot back over the live schedules, checked +set -euo pipefail +MODE=${1:?dry-run, execute or restore} +HOST=${ORBIT_BOX:-192.168.0.252:5555} +ROOT=$(cd "$(dirname "$0")/.." && pwd) +ADB="timeout 120 /opt/android-sdk/platform-tools/adb -s $HOST" +APP=tv.orbit.classic +STAMP=$(date -u +%Y%m%d-%H%M%S) +WORK=${ORBIT_CUTOVER_WORK:-$ROOT/../cutover-$STAMP} +say() { echo "[$(date +%H:%M:%S)] $*"; } +box() { $ADB shell run-as $APP "$@"; } + +timeout 15 /opt/android-sdk/platform-tools/adb connect "$HOST" >/dev/null 2>&1 || true +$ADB get-state >/dev/null || { say "box not reachable: switch wireless debugging on"; exit 1; } + +if [ "$MODE" = restore ]; then + SNAP=${2:?snapshot dir}; NAME=$(cat "$SNAP/device-copy.txt") + $ADB shell am force-stop $APP + box sh -c "cp -a no_backup/$NAME/. no_backup/schedule/" + box sh -c "cd no_backup/schedule && sha256sum $(cd "$SNAP/files" && ls | tr '\n' ' ')" | sort -k2 > "$SNAP/after-restore.sha256" + diff <(sort -k2 "$SNAP/manifest.sha256") "$SNAP/after-restore.sha256" && say "restored: every file matches the snapshot" + $ADB shell am start -n $APP/.MainActivity >/dev/null; exit 0 +fi +[ "$MODE" = dry-run ] || [ "${2:-}" = I-HAVE-PAULS-GO ] || { say "execute needs Paul's explicit go"; exit 1; } + +say "1. checking the box" +$ADB shell dumpsys package $APP | grep -m1 versionName +TOP=$($ADB shell dumpsys activity activities | grep -m1 topResumedActivity) +case "$TOP" in *orbit*|*youtube*|*netflix*) say "the TV is in use ($TOP): stopping"; exit 1;; esac + +say "2. stopping Orbit so nothing tops up during the change" +$ADB shell am force-stop $APP + +say "3. snapshot: this machine, the box, and a manifest" +mkdir -p "$WORK/snapshot/files" +FILES=$(box ls no_backup/schedule) +for f in $FILES; do $ADB exec-out run-as $APP cat "no_backup/schedule/$f" > "$WORK/snapshot/files/$f"; done +(cd "$WORK/snapshot/files" && sha256sum * | sort -k2) > "$WORK/snapshot/manifest.sha256" +box sh -c "cd no_backup/schedule && sha256sum *" | sort -k2 > "$WORK/snapshot/device.sha256" +diff "$WORK/snapshot/manifest.sha256" "$WORK/snapshot/device.sha256" >/dev/null || { say "copy on this machine differs from the box: stopping"; exit 1; } +NAME=schedule-rollback-$STAMP +box cp -a no_backup/schedule "no_backup/$NAME" +echo "$NAME" > "$WORK/snapshot/device-copy.txt" +box sh -c "cd no_backup/$NAME && sha256sum *" | sort -k2 > "$WORK/snapshot/device-copy.sha256" +diff "$WORK/snapshot/manifest.sha256" "$WORK/snapshot/device-copy.sha256" >/dev/null || { say "box copy differs: stopping"; exit 1; } +say " $(wc -l < "$WORK/snapshot/manifest.sha256") files, checksums match on both copies; box copy kept as no_backup/$NAME" + +say "4. rehearsing the restore on identical bytes" +box sh -c "cp -a no_backup/$NAME/. no_backup/schedule/" +box sh -c "cd no_backup/schedule && sha256sum *" | sort -k2 > "$WORK/snapshot/rehearsal.sha256" +diff "$WORK/snapshot/manifest.sha256" "$WORK/snapshot/rehearsal.sha256" >/dev/null || { say "rehearsal changed something: stopping"; exit 1; } +say " restore rehearsed: every file matches" + +say "5. computing the new schedules from this snapshot (every check must pass)" +T=$(( $(date +%s) * 1000 )) +mkdir -p "$WORK/new" +(cd "$ROOT/android" && GRADLE_USER_HOME="$ROOT/.tooling/gradle-cache" ANDROID_USER_HOME="$ROOT/.tooling/android" ANDROID_HOME=/opt/android-sdk \ + ORBIT_REPLAY_DIR="$WORK/snapshot/files" ORBIT_CUTOVER_OUT="$WORK/new" ORBIT_CUTOVER_AT=$T \ + ./gradlew --no-daemon --offline -q :app:testDebugUnitTest --rerun --tests '*CutoverSimulationTest*') || { say "a check failed: nothing written"; exit 1; } +mv "$WORK/snapshot/files/cutover-simulation.md" "$WORK/cutover-report.md" +(cd "$WORK/new" && sha256sum * | sort -k2) > "$WORK/new.sha256" +say " $(ls "$WORK/new" | wc -l) files computed; report: $WORK/cutover-report.md" + +if [ "$MODE" = dry-run ]; then + $ADB shell am start -a android.intent.action.MAIN -c android.intent.category.HOME >/dev/null 2>&1 + say "dry run complete: nothing new written. Orbit stays stopped until it's next opened."; exit 0 +fi + +say "6. writing the new schedules and checking them on the box" +$ADB shell mkdir -p /data/local/tmp/orbit-cutover +for f in "$WORK"/new/*; do $ADB push "$f" /data/local/tmp/orbit-cutover/ >/dev/null; done +box sh -c "cp /data/local/tmp/orbit-cutover/* no_backup/schedule/" +$ADB shell rm -rf /data/local/tmp/orbit-cutover +box sh -c "cd no_backup/schedule && sha256sum $(cd "$WORK/new" && ls | tr '\n' ' ')" | sort -k2 > "$WORK/written.sha256" +diff "$WORK/new.sha256" "$WORK/written.sha256" >/dev/null || { say "written files differ: restoring the snapshot"; "$0" restore "$WORK/snapshot"; exit 1; } +say " every written file matches" + +say "7. starting Orbit" +$ADB logcat -c; $ADB shell am start -n $APP/.MainActivity >/dev/null +say "done. Verify the log (added=0, clashes=0), playback and idents; the snapshot is in $WORK/snapshot and no_backup/$NAME"