#!/bin/sh set -eu if [ "$(id -u)" -ne 0 ]; then echo "update_walnutpi.sh must run as root" >&2 exit 1 fi if [ "$(uname -m)" != "aarch64" ]; then echo "this update is only for the WalnutPi AArch64 host" >&2 exit 1 fi SOURCE_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) PROJECT_ROOT=$(CDPATH= cd -- "$SOURCE_ROOT/.." && pwd) WHEELHOUSE=${WHEELHOUSE:-$PROJECT_ROOT/发布更新相关/其他依赖/aarch64-py311} FRPC_BUNDLE=${FRPC_BUNDLE:-$PROJECT_ROOT/发布更新相关/其他依赖/frp/0.71.0/linux-arm64} MANIFEST=$WHEELHOUSE/SHA256SUMS TARGET=/opt/matrix-screen-controller DATA_ROOT=/var/lib/matrix-screen-controller UNIT=/etc/systemd/system/matrix-screen-controller.service OTA_UNIT=/etc/systemd/system/matrix-screen-controller-ota.service SERVICE=matrix-screen-controller.service SOFTWARE_VERSION=$(tr -d '\r\n' < "$SOURCE_ROOT/VERSION") RELEASE_ID=${RELEASE_ID:-$SOFTWARE_VERSION-$(date -u +%Y%m%dT%H%M%SZ)} RELEASES=/opt/matrix-screen-controller.releases case "$RELEASE_ID" in *[!A-Za-z0-9._-]*|'') echo "RELEASE_ID may contain only letters, digits, dot, underscore, and dash" >&2 exit 1 ;; esac STAGING=/opt/matrix-screen-controller.stage.$RELEASE_ID BACKUP=/opt/matrix-screen-controller.rollback.$RELEASE_ID NEW_RELEASE=$RELEASES/$RELEASE_ID DATA_CANDIDATE=/var/lib/matrix-screen-controller.update.$RELEASE_ID DATA_BACKUP=/var/lib/matrix-screen-controller.rollback.$RELEASE_ID MIGRATION_RUNTIME=/run/matrix-screen-controller-update.$RELEASE_ID.migration STATUS_FILE=/run/matrix-screen-controller-update.$RELEASE_ID.status TEST_DATA=/run/matrix-screen-controller-update.$RELEASE_ID.test-data TEST_RUNTIME=/run/matrix-screen-controller-update.$RELEASE_ID.test-runtime FRPC_BACKUP=/run/matrix-screen-controller-update.$RELEASE_ID.frpc-backup SWAPPED=0 DATA_BACKED_UP=0 SUCCESS=0 SERVICE_STOPPED=0 FRPC_CHANGED=0 FRPC_WAS_ACTIVE=0 FRPC_WAS_ENABLED=0 PREVIOUS_LINK= cleanup_paths() { for path in "$STAGING" "$DATA_CANDIDATE" "$MIGRATION_RUNTIME" "$STATUS_FILE" "$TEST_DATA" "$TEST_RUNTIME" "$FRPC_BACKUP"; do case "$path" in /opt/matrix-screen-controller.stage.$RELEASE_ID|/var/lib/matrix-screen-controller.update.$RELEASE_ID|/run/matrix-screen-controller-update.$RELEASE_ID.migration|/run/matrix-screen-controller-update.$RELEASE_ID.status|/run/matrix-screen-controller-update.$RELEASE_ID.test-data|/run/matrix-screen-controller-update.$RELEASE_ID.test-runtime|/run/matrix-screen-controller-update.$RELEASE_ID.frpc-backup) if [ -d "$path" ]; then rm -rf -- "$path"; else rm -f -- "$path"; fi ;; esac done } rollback() { if [ "$SUCCESS" -eq 1 ]; then return fi if [ "$SWAPPED" -eq 1 ] || [ "$DATA_BACKED_UP" -eq 1 ]; then echo "new release failed; restoring previous program and persistent data" >&2 fi if [ "$SERVICE_STOPPED" -eq 1 ] || [ "$SWAPPED" -eq 1 ] || [ "$DATA_BACKED_UP" -eq 1 ]; then systemctl stop "$SERVICE" || true fi if [ "$DATA_BACKED_UP" -eq 1 ]; then FAILED_DATA=/var/lib/matrix-screen-controller.failed.$RELEASE_ID if [ -e "$DATA_ROOT" ] && [ ! -e "$FAILED_DATA" ]; then mv -- "$DATA_ROOT" "$FAILED_DATA"; fi if [ -d "$DATA_BACKUP" ]; then mv -- "$DATA_BACKUP" "$DATA_ROOT"; fi case "$FAILED_DATA" in /var/lib/matrix-screen-controller.failed.$RELEASE_ID) if [ -d "$FAILED_DATA" ]; then rm -rf -- "$FAILED_DATA"; fi ;; esac fi if [ "$FRPC_CHANGED" -eq 1 ]; then systemctl stop matrix-screen-frpc.service 2>/dev/null || true if [ -f "$FRPC_BACKUP/frpc" ]; then install -m 0755 "$FRPC_BACKUP/frpc" /usr/local/bin/frpc; else rm -f -- /usr/local/bin/frpc; fi if [ -f "$FRPC_BACKUP/unit" ]; then install -m 0644 "$FRPC_BACKUP/unit" /etc/systemd/system/matrix-screen-frpc.service; else rm -f -- /etc/systemd/system/matrix-screen-frpc.service; fi if [ -f "$FRPC_BACKUP/dropin" ]; then install -d -m 0755 /etc/systemd/system/matrix-screen-frpc.service.d install -m 0644 "$FRPC_BACKUP/dropin" /etc/systemd/system/matrix-screen-frpc.service.d/user.conf else rm -f -- /etc/systemd/system/matrix-screen-frpc.service.d/user.conf fi systemctl daemon-reload if [ "$FRPC_WAS_ENABLED" -eq 1 ]; then systemctl enable matrix-screen-frpc.service >/dev/null; else systemctl disable matrix-screen-frpc.service >/dev/null 2>&1 || true; fi if [ "$FRPC_WAS_ACTIVE" -eq 1 ]; then systemctl start matrix-screen-frpc.service || true; fi fi if [ "$SWAPPED" -eq 1 ]; then if [ -L "$TARGET" ]; then rm -f -- "$TARGET" elif [ -d "$TARGET" ]; then FAILED=/opt/matrix-screen-controller.failed.$RELEASE_ID if [ ! -e "$FAILED" ]; then mv -- "$TARGET" "$FAILED"; fi fi if [ -d "$BACKUP" ]; then mv -- "$BACKUP" "$TARGET" elif [ -n "$PREVIOUS_LINK" ]; then ln -s -- "$PREVIOUS_LINK" "$TARGET" fi rm -rf -- "$NEW_RELEASE" install -m 0644 "$TARGET/systemd/matrix-screen-controller.service" "$UNIT" install -m 0644 "$TARGET/systemd/matrix-screen-controller-ota.service" "$OTA_UNIT" systemctl daemon-reload fi if [ "$SERVICE_STOPPED" -eq 1 ] || [ "$SWAPPED" -eq 1 ] || [ "$DATA_BACKED_UP" -eq 1 ]; then systemctl start "$SERVICE" || true fi } finish() { code=$? trap - EXIT HUP INT TERM rollback cleanup_paths exit "$code" } trap finish EXIT HUP INT TERM if [ ! -f "$MANIFEST" ]; then echo "offline wheel manifest missing: $MANIFEST" >&2 exit 1 fi if [ ! -d "$TARGET" ]; then echo "existing production target missing: $TARGET" >&2 exit 1 fi if [ ! -d "$DATA_ROOT" ]; then echo "existing persistent data root missing: $DATA_ROOT" >&2 exit 1 fi if [ -e "$STAGING" ] || [ -e "$BACKUP" ] || [ -e "$NEW_RELEASE" ] || [ -e "$DATA_CANDIDATE" ] || [ -e "$DATA_BACKUP" ]; then echo "release staging or rollback path already exists for $RELEASE_ID" >&2 exit 1 fi (cd "$WHEELHOUSE" && sha256sum -c SHA256SUMS) mkdir -m 0750 -- "$STAGING" cp -a "$SOURCE_ROOT/." "$STAGING/" python3 -m venv "$STAGING/.venv" "$STAGING/.venv/bin/pip" install \ --no-index \ --find-links "$WHEELHOUSE" \ -r "$STAGING/requirements-dev.txt" make -C "$STAGING/app/display/native" clean all test "$STAGING/.venv/bin/python" -m compileall -q "$STAGING/app" mkdir -p -- "$TEST_DATA" "$TEST_RUNTIME/tmp" (cd "$STAGING" && \ MATRIX_DRIVER=mock \ MATRIX_TEST_ROOT="$TEST_DATA" \ MATRIX_DATA_DIR="$TEST_DATA" \ MATRIX_RUNTIME_DIR="$TEST_RUNTIME" \ MATRIX_SOURCE_ONLY_UPDATE_TESTS=1 \ TMPDIR="$TEST_RUNTIME/tmp" \ TEMP="$TEST_RUNTIME/tmp" \ TMP="$TEST_RUNTIME/tmp" \ "$STAGING/.venv/bin/python" -m pytest -p no:cacheprovider -q \ --basetemp="$TEST_RUNTIME/pytest" --tb=short --disable-warnings) "$STAGING/.venv/bin/python" "$STAGING/scripts/dedicated_host.py" check systemctl stop "$SERVICE" SERVICE_STOPPED=1 cp -a -- "$DATA_ROOT" "$DATA_CANDIDATE" PYTHONPATH="$STAGING" "$STAGING/.venv/bin/python" -m scripts.prepare_data_root \ --data-root "$DATA_CANDIDATE" \ --runtime-root "$MIGRATION_RUNTIME" mkdir -p -- "$RELEASES" if [ -L "$TARGET" ]; then PREVIOUS_LINK=$(readlink "$TARGET") rm -f -- "$TARGET" SWAPPED=1 else mv -- "$TARGET" "$BACKUP" SWAPPED=1 fi mv -- "$STAGING" "$NEW_RELEASE" ln -s -- "$NEW_RELEASE" "$TARGET" mv -- "$DATA_ROOT" "$DATA_BACKUP" DATA_BACKED_UP=1 mv -- "$DATA_CANDIDATE" "$DATA_ROOT" install -m 0644 "$TARGET/systemd/matrix-screen-controller.service" "$UNIT" install -m 0644 "$TARGET/systemd/matrix-screen-controller-ota.service" "$OTA_UNIT" install -d -m 0700 "$FRPC_BACKUP" systemctl is-active --quiet matrix-screen-frpc.service 2>/dev/null && FRPC_WAS_ACTIVE=1 || true systemctl is-enabled --quiet matrix-screen-frpc.service 2>/dev/null && FRPC_WAS_ENABLED=1 || true if [ -f /usr/local/bin/frpc ]; then cp -p -- /usr/local/bin/frpc "$FRPC_BACKUP/frpc"; fi if [ -f /etc/systemd/system/matrix-screen-frpc.service ]; then cp -p -- /etc/systemd/system/matrix-screen-frpc.service "$FRPC_BACKUP/unit"; fi if [ -f /etc/systemd/system/matrix-screen-frpc.service.d/user.conf ]; then cp -p -- /etc/systemd/system/matrix-screen-frpc.service.d/user.conf "$FRPC_BACKUP/dropin"; fi FRPC_CHANGED=1 FRPC_BUNDLE="$FRPC_BUNDLE" /bin/sh "$TARGET/scripts/install_frpc_system.sh" systemctl daemon-reload systemctl start "$SERVICE" check_health() { attempt=0 while [ "$attempt" -lt 30 ]; do if curl --fail --silent http://127.0.0.1:8080/api/status >"$STATUS_FILE"; then if "$TARGET/.venv/bin/python" - "$STATUS_FILE" <<'PY' import json from pathlib import Path import sys status = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) screen = status.get("screen", {}) driver = screen.get("driver_status") or {} expected_version = Path("/opt/matrix-screen-controller/VERSION").read_text(encoding="utf-8").strip() assert status.get("service", {}).get("software_version") == expected_version assert screen.get("driver") == "walnutpi-h618-hub75" assert screen.get("hardware_mapping") == "walnutpi-pi-bank-pwm-oe-v2" assert driver.get("oe_timing_backend") == "h618-pwm4" assert driver.get("oe_pulse_faults") == 0 PY then return 0 fi fi attempt=$((attempt + 1)) sleep 1 done return 1 } if ! check_health; then echo "new release did not pass the production health check" >&2 exit 1 fi systemctl restart "$SERVICE" if ! check_health; then echo "new release did not pass the restart health check" >&2 exit 1 fi SUCCESS=1 SWAPPED=0 DATA_BACKED_UP=0 SERVICE_STOPPED=0 FRPC_CHANGED=0 case "$BACKUP" in /opt/matrix-screen-controller.rollback.$RELEASE_ID) rm -rf -- "$BACKUP" ;; esac case "$DATA_BACKUP" in /var/lib/matrix-screen-controller.rollback.$RELEASE_ID) rm -rf -- "$DATA_BACKUP" ;; esac case "$PREVIOUS_LINK" in /opt/matrix-screen-controller.releases/*) if [ "$PREVIOUS_LINK" != "$NEW_RELEASE" ]; then rm -rf -- "$PREVIOUS_LINK"; fi ;; esac systemctl --no-pager --full status "$SERVICE" echo "WalnutPi Matrix Screen Controller updated successfully: $RELEASE_ID"