Pulse/scripts/pulse-auto-update.sh
rcourtman c0b3a0e665 Restart Pulse service after failed auto-update (#1323)
The auto-update flow stops the Pulse service before applying updates.
If the update fails, the rollback path restored files but never
restarted the service. Since the main unit was explicitly stopped
(not crashed), systemd's Restart=always didn't rescue it.

Add restart-on-failure guards to both pulse-auto-update.sh and
install.sh so Pulse is always restarted after a failed update attempt.
2026-03-07 10:46:19 +00:00

321 lines
10 KiB
Bash
Executable file

#!/bin/bash
# Pulse Automatic Update Script
# This script checks for and installs stable Pulse updates
# It is designed to be run by systemd timer
set -euo pipefail
# Configuration
GITHUB_REPO="rcourtman/Pulse"
INSTALL_DIR="/opt/pulse"
CONFIG_DIR="/etc/pulse"
LOG_TAG="pulse-auto-update"
MAX_LOG_SIZE=10485760 # 10MB
# Logging function
log() {
local level=$1
shift
logger -t "$LOG_TAG" -p "user.$level" "$@"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $@"
}
# Check if auto-updates are enabled
check_auto_updates_enabled() {
# Check system.json for autoUpdateEnabled flag (note: no 's' - matches Go struct)
if [[ -f "$CONFIG_DIR/system.json" ]]; then
local enabled=$(cat "$CONFIG_DIR/system.json" 2>/dev/null | grep -o '"autoUpdateEnabled"[[:space:]]*:[[:space:]]*true' || true)
if [[ -z "$enabled" ]]; then
log info "Auto-updates disabled in configuration"
exit 0
fi
fi
# Also check if timer is enabled (belt and suspenders)
if ! systemctl is-enabled --quiet pulse-update.timer 2>/dev/null; then
log info "Auto-update timer is disabled"
exit 0
fi
}
# Get current version
get_current_version() {
local version=""
# Try to get version from binary
if [[ -f "$INSTALL_DIR/bin/pulse" ]]; then
version=$("$INSTALL_DIR/bin/pulse" --version 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.]+)?' | head -1 || true)
elif [[ -f "$INSTALL_DIR/pulse" ]]; then
version=$("$INSTALL_DIR/pulse" --version 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.]+)?' | head -1 || true)
fi
# Fallback to VERSION file
if [[ -z "$version" ]] && [[ -f "$INSTALL_DIR/VERSION" ]]; then
version=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null | tr -d '\n' || true)
fi
echo "${version:-unknown}"
}
# Get latest stable release from GitHub
get_latest_stable_version() {
local latest_version=""
# Get latest stable release (not pre-releases)
latest_version=$(curl -s "https://api.github.com/repos/$GITHUB_REPO/releases/latest" | \
grep '"tag_name":' | \
sed -E 's/.*"([^"]+)".*/\1/' || true)
# Check if we got rate limited or failed
if [[ -z "$latest_version" ]] || [[ "$latest_version" == *"rate limit"* ]]; then
# Try direct GitHub latest URL as fallback
latest_version=$(curl -sI "https://github.com/$GITHUB_REPO/releases/latest" | \
grep -i '^location:' | \
sed -E 's|.*tag/([^[:space:]]+).*|\1|' | \
tr -d '\r' || true)
fi
echo "${latest_version:-}"
}
# Compare versions (returns 0 if v1 > v2, 1 if v1 <= v2)
version_greater_than() {
local v1="${1#v}" # Remove 'v' prefix
local v2="${2#v}"
# Don't update if current version is unknown
if [[ "$2" == "unknown" ]]; then
return 1
fi
# Split versions into parts
IFS='.' read -ra V1_PARTS <<< "${v1%%-*}" # Remove pre-release suffix
IFS='.' read -ra V2_PARTS <<< "${v2%%-*}"
# Compare major.minor.patch
for i in 0 1 2; do
local p1="${V1_PARTS[$i]:-0}"
local p2="${V2_PARTS[$i]:-0}"
if [[ "$p1" -gt "$p2" ]]; then
return 0
elif [[ "$p1" -lt "$p2" ]]; then
return 1
fi
done
# Versions are equal in major.minor.patch
# Now check pre-release suffixes
local has_suffix1=false
local has_suffix2=false
[[ "$v1" == *-* ]] && has_suffix1=true
[[ "$v2" == *-* ]] && has_suffix2=true
# Stable (no suffix) > pre-release (has suffix)
if [[ "$has_suffix1" == "false" ]] && [[ "$has_suffix2" == "true" ]]; then
return 0 # v1 (stable) > v2 (pre-release)
elif [[ "$has_suffix1" == "true" ]] && [[ "$has_suffix2" == "false" ]]; then
return 1 # v1 (pre-release) < v2 (stable)
elif [[ "$has_suffix1" == "true" ]] && [[ "$has_suffix2" == "true" ]]; then
# Both are pre-releases, compare suffixes lexicographically
local suffix1="${v1#*-}"
local suffix2="${v2#*-}"
if [[ "$suffix1" > "$suffix2" ]]; then
return 0
fi
fi
return 1 # v1 <= v2
}
# Detect service name (could be pulse or pulse-backend)
detect_service_name() {
if systemctl list-unit-files --no-legend | grep -q "^pulse-backend.service"; then
echo "pulse-backend"
elif systemctl list-unit-files --no-legend | grep -q "^pulse.service"; then
echo "pulse"
else
echo "pulse" # Default
fi
}
restart_service_if_needed() {
local service_name=$1
local service_was_active=$2
if [[ "$service_was_active" == "true" ]]; then
log info "Starting Pulse service after failed update"
systemctl start "$service_name" || true
fi
}
# Perform the update
perform_update() {
local new_version=$1
local service_name=$(detect_service_name)
local service_was_active=false
if systemctl is-active --quiet "$service_name" 2>/dev/null; then
service_was_active=true
fi
log info "Starting update to $new_version"
# Create backup of current installation
local backup_dir="/tmp/pulse-backup-$(date +%Y%m%d-%H%M%S)"
log info "Creating backup in $backup_dir"
mkdir -p "$backup_dir"
# Backup binary
if [[ -f "$INSTALL_DIR/bin/pulse" ]]; then
cp -a "$INSTALL_DIR/bin/pulse" "$backup_dir/" || true
elif [[ -f "$INSTALL_DIR/pulse" ]]; then
cp -a "$INSTALL_DIR/pulse" "$backup_dir/" || true
fi
# Backup VERSION file
if [[ -f "$INSTALL_DIR/VERSION" ]]; then
cp -a "$INSTALL_DIR/VERSION" "$backup_dir/" || true
fi
# Download update using install script (safest method)
log info "Downloading and installing update"
# Run install script with specific version
local marker_file="$INSTALL_DIR/BUILD_FROM_SOURCE"
local -a installer_args=(--version "$new_version")
if [[ -f "$marker_file" ]]; then
local branch
branch=$(tr -d '\r\n' <"$marker_file" 2>/dev/null || true)
if [[ -n "$branch" ]]; then
installer_args=(--source "$branch" "${installer_args[@]}")
fi
fi
if curl -sSL "https://github.com/$GITHUB_REPO/releases/latest/download/install.sh" | \
bash -s -- "${installer_args[@]}" 2>&1 | \
while IFS= read -r line; do
log info "installer: $line"
done; then
log info "Update successfully installed"
# Verify new version
local installed_version=$(get_current_version)
if [[ "$installed_version" == "$new_version" ]]; then
log info "Version verified: $installed_version"
# Clean up backup
rm -rf "$backup_dir"
return 0
else
log error "Version mismatch after update. Expected: $new_version, Got: $installed_version"
# Restore from backup
log info "Restoring from backup"
if [[ -f "$backup_dir/pulse" ]]; then
if [[ -f "$INSTALL_DIR/bin/pulse" ]]; then
cp -f "$backup_dir/pulse" "$INSTALL_DIR/bin/pulse"
else
cp -f "$backup_dir/pulse" "$INSTALL_DIR/pulse"
fi
fi
if [[ -f "$backup_dir/VERSION" ]]; then
cp -f "$backup_dir/VERSION" "$INSTALL_DIR/VERSION"
fi
restart_service_if_needed "$service_name" "$service_was_active"
# Clean up backup
rm -rf "$backup_dir"
return 1
fi
else
log error "Update installation failed"
# Restore from backup
log info "Restoring from backup"
if [[ -f "$backup_dir/pulse" ]]; then
if [[ -f "$INSTALL_DIR/bin/pulse" ]]; then
cp -f "$backup_dir/pulse" "$INSTALL_DIR/bin/pulse"
else
cp -f "$backup_dir/pulse" "$INSTALL_DIR/pulse"
fi
fi
if [[ -f "$backup_dir/VERSION" ]]; then
cp -f "$backup_dir/VERSION" "$INSTALL_DIR/VERSION"
fi
restart_service_if_needed "$service_name" "$service_was_active"
# Clean up backup
rm -rf "$backup_dir"
return 1
fi
}
# Main update check
main() {
log info "Starting Pulse auto-update check"
# Check if auto-updates are enabled
check_auto_updates_enabled
# Check if we're in Docker (updates not supported)
if [[ -f /.dockerenv ]] || grep -q docker /proc/1/cgroup 2>/dev/null; then
log info "Docker environment detected, skipping auto-update"
exit 0
fi
# Get current version
local current_version=$(get_current_version)
log info "Current version: $current_version"
if [[ "$current_version" == "unknown" ]]; then
log error "Could not determine current version, skipping update"
exit 1
fi
# Get latest stable version
local latest_version=$(get_latest_stable_version)
if [[ -z "$latest_version" ]]; then
log error "Could not determine latest version from GitHub"
exit 1
fi
log info "Latest stable version: $latest_version"
# Compare versions
if version_greater_than "$latest_version" "$current_version"; then
log info "New version available: $latest_version (current: $current_version)"
# Perform update
if perform_update "$latest_version"; then
log info "Update completed successfully to $latest_version"
# Send notification if webhooks are configured
if [[ -f "$CONFIG_DIR/webhooks.enc" ]] || [[ -f "$CONFIG_DIR/webhooks.json" ]]; then
# Create a simple notification via the Pulse API
curl -s -X POST "http://localhost:7655/api/internal/notification" \
-H "Content-Type: application/json" \
-d "{\"type\":\"update\",\"message\":\"Pulse automatically updated from $current_version to $latest_version\"}" \
2>/dev/null || true
fi
else
log error "Update failed"
exit 1
fi
else
log info "Already running latest version"
fi
log info "Auto-update check completed"
}
# Run main function
main "$@"