#!/bin/bash set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" PHOENIX_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)" PHOENIX_PARENT="$(cd "$PHOENIX_ROOT/.." && pwd -P)" PHOENIX_NAME="$(basename "$PHOENIX_ROOT")" STAGE_DIR="$PHOENIX_PARENT/${PHOENIX_NAME}.update" BACKUP_DIR="$PHOENIX_PARENT/${PHOENIX_NAME}.backup" FAILED_DIR="$PHOENIX_PARENT/${PHOENIX_NAME}.failed" LOG_FILE="$PHOENIX_PARENT/${PHOENIX_NAME}.update.log" exec >>"$LOG_FILE" 2>&1 echo "=== $(date): restart_phoenix_services.sh START ===" restart_if_present() { local unit="$1" if ! command -v systemctl >/dev/null 2>&1; then return 0 fi if run_systemctl list-unit-files "$unit" >/dev/null 2>&1 || run_systemctl status "$unit" >/dev/null 2>&1; then run_systemctl restart "$unit" >/dev/null 2>&1 || true fi } stop_if_present() { local unit="$1" if ! command -v systemctl >/dev/null 2>&1; then return 0 fi if run_systemctl list-unit-files "$unit" >/dev/null 2>&1 || run_systemctl status "$unit" >/dev/null 2>&1; then run_systemctl stop "$unit" >/dev/null 2>&1 || true fi } run_systemctl() { if [ "$(id -u)" -eq 0 ]; then systemctl "$@" return $? fi if ! command -v sudo >/dev/null 2>&1; then echo "sudo not found; cannot control system services as non-root" >&2 return 1 fi sudo -n systemctl "$@" } run_systemd_run() { local unit_name="phoenix-online-update-$(date +%s)" if [ "$(id -u)" -eq 0 ]; then systemd-run --unit="$unit_name" --collect --property=Type=oneshot \ /usr/bin/env PHOENIX_UPDATE_DETACHED=1 /bin/bash "$0" return $? fi if ! command -v sudo >/dev/null 2>&1; then echo "sudo not found; cannot detach update restart as non-root" >&2 return 1 fi sudo -n systemd-run --unit="$unit_name" --collect --property=Type=oneshot \ /usr/bin/env PHOENIX_UPDATE_DETACHED=1 /bin/bash "$0" } health_ok() { if ! command -v curl >/dev/null 2>&1; then return 0 fi curl -fsS http://127.0.0.1:8789/health >/dev/null 2>&1 } needs_phoenix_build() { local phoenix_dir="${1:-$PHOENIX_ROOT}" local phoenix_bin="$phoenix_dir/target/release/phoenix" if [ ! -x "$phoenix_bin" ] || [ ! -s "$phoenix_bin" ]; then return 0 fi if [ -f "$phoenix_dir/Cargo.toml" ] && [ "$phoenix_dir/Cargo.toml" -nt "$phoenix_bin" ]; then return 0 fi if [ -f "$phoenix_dir/Cargo.lock" ] && [ "$phoenix_dir/Cargo.lock" -nt "$phoenix_bin" ]; then return 0 fi if [ -d "$phoenix_dir/src" ] && find "$phoenix_dir/src" -type f -name '*.rs' -newer "$phoenix_bin" -print -quit | grep -q .; then return 0 fi return 1 } clear_browser_asset_caches() { local cache_roots=( "/data/chrome-analyzer/Default/Cache" "/data/chrome-analyzer/Default/Code Cache" "/data/chrome-analyzer/Default/GPUCache" "/data/chrome-dualtabs/Default/Cache" "/data/chrome-dualtabs/Default/Code Cache" "/data/chrome-dualtabs/Default/GPUCache" ) local path for path in "${cache_roots[@]}"; do if [ -e "$path" ]; then rm -rf "$path" || true fi done } wait_for_health() { local attempts="${1:-30}" local delay="${2:-1}" local i for i in $(seq 1 "$attempts"); do if health_ok; then return 0 fi sleep "$delay" done return 1 } wait_for_candidate_health() { local phoenix_dir="${1:-$PHOENIX_ROOT}" local attempts="${2:-45}" local delay="${3:-1}" local start_try local max_start_tries="${PHOENIX_UPDATE_START_TRIES:-3}" if needs_phoenix_build "$phoenix_dir"; then attempts="${PHOENIX_UPDATE_BUILD_HEALTH_ATTEMPTS:-420}" echo "Phoenix tree requires local rebuild; extending health wait to ${attempts}s" else echo "Phoenix tree already has runnable binary; health wait ${attempts}s" fi for start_try in $(seq 1 "$max_start_tries"); do echo "starting Phoenix services health attempt ${start_try}/${max_start_tries}" start_all_services if wait_for_health "$attempts" "$delay"; then echo "Phoenix health OK on attempt ${start_try}/${max_start_tries}" return 0 fi echo "Phoenix health failed on attempt ${start_try}/${max_start_tries}" stop_all_services done return 1 } start_all_services() { restart_if_present phoenix.service restart_if_present analyzer-web.service restart_if_present analyzer-web-volumio.service clear_browser_asset_caches restart_if_present analyzer-kiosk.service restart_if_present analyzer-kiosk-volumio.service } stop_all_services() { stop_if_present analyzer-kiosk.service stop_if_present analyzer-kiosk-volumio.service stop_if_present analyzer-web.service stop_if_present analyzer-web-volumio.service stop_if_present phoenix.service } rollback_to_backup() { stop_all_services rm -rf "$FAILED_DIR" if [ -d "$PHOENIX_ROOT" ]; then mv "$PHOENIX_ROOT" "$FAILED_DIR" fi if [ ! -d "$BACKUP_DIR" ]; then echo "backup missing; cannot roll back to $BACKUP_DIR" >&2 return 1 fi mv "$BACKUP_DIR" "$PHOENIX_ROOT" if ! wait_for_candidate_health "$PHOENIX_ROOT" "${PHOENIX_ROLLBACK_HEALTH_ATTEMPTS:-120}" 1; then echo "backup rollback restored files but Phoenix health did not recover" >&2 return 1 fi echo "rollback to backup succeeded" } if [ "${PHOENIX_UPDATE_DETACHED:-0}" != "1" ]; then if ! command -v systemd-run >/dev/null 2>&1; then echo "systemd-run not found; cannot safely detach updater" >&2 exit 1 fi echo "detaching updater into transient systemd unit" if run_systemd_run; then echo "detached updater started" exit 0 fi echo "failed to start detached updater" >&2 exit 1 fi if [ ! -d "$STAGE_DIR" ]; then echo "staged update missing: $STAGE_DIR" >&2 exit 1 fi if [ ! -f "$STAGE_DIR/Cargo.toml" ] || [ ! -d "$STAGE_DIR/src" ] || [ ! -d "$STAGE_DIR/www" ] || [ ! -d "$STAGE_DIR/scripts" ]; then echo "staged update invalid: $STAGE_DIR" >&2 exit 1 fi stop_all_services rm -rf "$BACKUP_DIR" if [ -d "$PHOENIX_ROOT" ]; then mv "$PHOENIX_ROOT" "$BACKUP_DIR" fi mv "$STAGE_DIR" "$PHOENIX_ROOT" health_attempts=45 health_delay=1 if ! wait_for_candidate_health "$PHOENIX_ROOT" "$health_attempts" "$health_delay"; then if ! rollback_to_backup; then echo "new Phoenix version failed health check and rollback failed" >&2 exit 1 fi echo "new Phoenix version failed health check; rolled back to backup" >&2 exit 1 fi echo "=== $(date): restart_phoenix_services.sh OK ==="