Files
pulse/scripts/install-pulse.sh
T
courtmanr@gmail.com 25ea0cc258 fix: add CSS file recovery mechanism for failed tarball extractions
Enhanced the CSS verification to actively recover the CSS file if it's
missing, empty, or corrupted after tarball extraction. The script now
attempts direct extraction of just the CSS file from the release tarball
as a fallback mechanism.

This ensures that even if the main tarball extraction has issues, users
will still get a working CSS file and proper frontend styling.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-05-29 21:53:44 +01:00

2228 lines
90 KiB
Bash
Executable File

#!/bin/bash
NODE_MAJOR_VERSION=20
PULSE_DIR="/opt/pulse-proxmox"
PULSE_USER="pulse"
SERVICE_NAME="pulse-monitor.service"
SCRIPT_NAME="install-pulse.sh"
LOG_FILE="/var/log/pulse_update.log"
SCRIPT_ABS_PATH=""
REPO_URL="https://github.com/rcourtman/Pulse.git"
SCRIPT_RAW_URL="https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/install-pulse.sh"
CURRENT_SCRIPT_COMMIT_SHA="5d60cc0"
INSTALLER_VERSION="0.0.0"
MODE_UPDATE=""
INSTALL_MODE=""
SPECIFIED_VERSION_TAG=""
TARGET_TAG=""
SPECIFIED_BRANCH=""
TARGET_BRANCH=""
INSTALLER_WAS_REEXECUTED=false
if command -v readlink &> /dev/null && readlink -f "$0" &> /dev/null; then
SCRIPT_ABS_PATH=$(readlink -f "$0")
else
if [[ "$0" == /* ]]; then
SCRIPT_ABS_PATH="$0"
else
SCRIPT_ABS_PATH="$(pwd)/$0"
fi
if [ ! -f "$SCRIPT_ABS_PATH" ]; then
SCRIPT_ABS_PATH=""
fi
fi
while [[ "$#" -gt 0 ]]; do
case $1 in
--update) MODE_UPDATE="true"; shift ;;
--installer-reexecuted)
INSTALLER_WAS_REEXECUTED=true
shift
;;
--version)
if [[ -n "$2" ]] && [[ "$2" != --* ]]; then
SPECIFIED_VERSION_TAG="$2"
shift 2
else
echo "Error: --version requires a tag name [e.g., v3.2.3]" >&2
exit 1
fi
;;
--branch)
if [[ -n "$2" ]] && [[ "$2" != --* ]]; then
SPECIFIED_BRANCH="$2"
shift 2
else
echo "Error: --branch requires a branch name [e.g., feature/pve-backups]" >&2
exit 1
fi
;;
*) echo "Unknown parameter passed: $1"; exit 1 ;;
esac
done
# Validate that both --version and --branch aren't specified
if [[ -n "$SPECIFIED_VERSION_TAG" ]] && [[ -n "$SPECIFIED_BRANCH" ]]; then
echo "Error: Cannot specify both --version and --branch. Please choose one." >&2
exit 1
fi
print_info() {
echo -e "\033[1;34m[INFO]\033[0m $1"
}
print_success() {
echo -e "\033[1;32m[SUCCESS]\033[0m $1"
}
print_warning() {
echo -e "\033[1;33m[WARNING]\033[0m $1"
}
print_error() {
echo -e "\033[1;31m[ERROR]\033[0m $1" >&2
}
check_root() {
if [ "$(id -u)" -ne 0 ]; then
print_error "This script must be run as root. Please use sudo."
exit 1
fi
}
print_dependency_info() {
echo -e "\033[1;34m"
echo ' ____ _ '
echo '| _ \ _ _| |___ ___ '
echo '| |_) | | | | / __|/ _ \'
echo '| __/| |_| | \__ \ __/'
echo '|_| \__,_|_|___/\___|'
echo ' '
echo -e "\033[0m"
echo ""
echo -e "\033[1mWelcome to the Pulse for Proxmox VE Installer\033[0m"
echo "This script will install Pulse, a lightweight monitoring application."
echo "See README.md for more details about Pulse: https://github.com/rcourtman/Pulse"
echo ""
local dep_text
dep_text=$(cat << 'EOF'
-----------------------------------------------------
Required Dependencies:
Core Tools: curl, git, sudo, gpg, diffutils (via apt)
Pulse Runtime: Node.js & npm (via NodeSource)
Installer Self-Update: jq (via apt)
-----------------------------------------------------
EOF
)
print_info "$dep_text"
local confirm_proceed
read -p "Do you want to proceed with the installation/update? [Y/n]: " confirm_proceed
if [[ "$confirm_proceed" =~ ^[Nn]$ ]]; then
print_error "Operation aborted by user."
exit 1
fi
print_info "Proceeding..."
}
self_update_check() {
if [ ! -t 0 ] || [ -n "$MODE_UPDATE" ] || [ -z "$SCRIPT_ABS_PATH" ]; then
return 0
fi
if ! command -v curl &> /dev/null; then
print_warning "curl command not found, skipping installer self-update check."
return 0
fi
if ! command -v jq &> /dev/null; then
print_info "jq command not found, attempting to install it for self-update check..."
if command -v apt-get &> /dev/null; then
apt-get update -qq > /dev/null
if apt-get install -y -qq jq > /dev/null; then
print_success "jq installed successfully."
else
local jq_install_exit_code=$?
print_warning "Failed to automatically install jq (Exit code: $jq_install_exit_code)."
print_warning "Please install jq manually (e.g., sudo apt-get install jq) to enable installer updates."
print_warning "Skipping installer self-update check for this run."
return 0
fi
else
print_warning "apt-get not found. Cannot automatically install jq."
print_warning "Please install jq manually to enable installer updates."
print_warning "Skipping installer self-update check for this run."
return 0
fi
fi
local git_ok=false
if [ -d "$PULSE_DIR/.git" ] && command -v git &> /dev/null; then
git_ok=true
fi
if [ "$git_ok" = false ]; then
if [ -d "$PULSE_DIR/.git" ]; then
print_warning "git command not found within $PULSE_DIR, skipping installer self-update check."
return 0
else
print_info "Git not found or $PULSE_DIR not a repo yet, proceeding with API check."
fi
fi
print_info "Checking for updates to the installer script itself (using GitHub API)..."
local owner="rcourtman"
local repo="Pulse"
local script_relative_path="scripts/install-pulse.sh"
local branch="main"
local api_url="https://api.github.com/repos/${owner}/${repo}/commits?path=${script_relative_path}&sha=${branch}&per_page=1"
local latest_remote_sha
latest_remote_sha=$(curl -sL -H "Accept: application/vnd.github.v3+json" "$api_url" | jq -r 'if type=="array" and length > 0 then .[0].sha else empty end')
local curl_exit_code=$?
if [ $curl_exit_code -ne 0 ]; then
print_warning "curl command failed when checking GitHub API for updates (Exit code: $curl_exit_code). Skipping self-update check."
return 0
fi
if [ -z "$latest_remote_sha" ] || [ "$latest_remote_sha" = "null" ]; then
print_warning "Could not determine the latest commit SHA from GitHub API. Skipping self-update check."
return 0
fi
local current_local_sha="$CURRENT_SCRIPT_COMMIT_SHA"
if [ -z "$current_local_sha" ]; then
print_warning "Could not find embedded CURRENT_SCRIPT_COMMIT_SHA in the script. Skipping self-update check."
return 0
fi
if [ "$latest_remote_sha" != "$current_local_sha" ]; then
print_warning "A newer version of the installation script (Remote: ${latest_remote_sha:0:7}, Local: ${current_local_sha:0:7}) is available."
read -p "Do you want to update the installer and re-run? [Y/n]: " update_confirm
if [[ ! "$update_confirm" =~ ^[Nn]$ ]]; then
print_info "Updating installer script..."
local temp_script="/tmp/${SCRIPT_NAME}.tmp"
if ! curl -sL "$SCRIPT_RAW_URL" -o "$temp_script"; then
print_error "Failed to download the latest installer script from $SCRIPT_RAW_URL."
rm -f "$temp_script"
return 1
fi
print_info "[DEBUG] Updating embedded SHA in temp script to $latest_remote_sha..."
local processed_temp_script="${temp_script}.processed"
local line_updated=false
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" == CURRENT_SCRIPT_COMMIT_SHA=* ]]; then
echo "CURRENT_SCRIPT_COMMIT_SHA=\"$latest_remote_sha\"" >> "$processed_temp_script"
line_updated=true
else
echo "$line" >> "$processed_temp_script"
fi
done < "$temp_script"
if [ "$line_updated" = false ]; then
print_error "Failed to find and update CURRENT_SCRIPT_COMMIT_SHA line in downloaded script!"
rm -f "$temp_script" "$processed_temp_script"
return 1
fi
if ! chmod +x "$processed_temp_script"; then
print_error "Failed to make processed temporary script executable."
rm -f "$temp_script" "$processed_temp_script"
return 1
fi
if ! mv "$processed_temp_script" "$SCRIPT_ABS_PATH"; then
print_error "Failed to replace the current script file with processed version."
rm -f "$temp_script" "$processed_temp_script"
return 1
fi
rm -f "$temp_script"
print_success "Installer updated successfully to commit ${latest_remote_sha:0:7}."
print_info "Re-executing with updated installer..."
exec bash "$SCRIPT_ABS_PATH" --installer-reexecuted "$@"
print_error "Failed to re-execute the updated script. Please re-run manually: sudo bash $SCRIPT_ABS_PATH"
exit 1
else
print_info "Skipping installer update. Continuing with the current version."
return 0
fi
else
print_info "Installer script is up-to-date (Commit: $current_local_sha)"
return 0
fi
}
get_latest_remote_tag() {
local latest_tag
print_info "Fetching latest remote tags..." >&2
if ! sudo -u "$PULSE_USER" git fetch origin --tags --force >/dev/null 2>&1; then
print_warning "Could not fetch remote tags." >&2
return 1
fi
latest_tag=$(sudo -u "$PULSE_USER" git tag -l 'v*' --sort='-version:refname' | head -n 1)
if [ -z "$latest_tag" ]; then
print_warning "Could not determine the latest remote release tag." >&2
return 1
fi
echo "$latest_tag"
return 0
}
check_remote_tag_exists() {
local tag_to_check=$1
if [ -z "$tag_to_check" ]; then return 1; fi
print_info "Checking if remote tag '$tag_to_check' exists..."
if sudo -u "$PULSE_USER" git ls-remote --tags origin "refs/tags/$tag_to_check" | grep -q "refs/tags/$tag_to_check$"; then
print_info "Remote tag '$tag_to_check' found."
return 0
else
print_error "Remote tag '$tag_to_check' not found."
return 1
fi
}
get_current_local_tag() {
local current_tag
current_tag=$(sudo -u "$PULSE_USER" git describe --tags --exact-match HEAD 2>/dev/null)
if [ $? -ne 0 ]; then
current_tag=$(sudo -u "$PULSE_USER" git describe --tags --abbrev=0 HEAD 2>/dev/null)
if [ $? -ne 0 ]; then
current_tag=""
fi
fi
echo "$current_tag"
}
check_release_exists() {
local tag_to_check=$1
if [ -z "$tag_to_check" ]; then return 1; fi
print_info "Checking if release '$tag_to_check' has available assets..."
local release_url="https://api.github.com/repos/rcourtman/Pulse/releases/tags/$tag_to_check"
if command -v curl >/dev/null 2>&1; then
if curl -sSf "$release_url" >/dev/null 2>&1; then
print_info "Release '$tag_to_check' found with assets."
return 0
fi
fi
print_warning "Release '$tag_to_check' not found or no assets available."
return 1
}
download_and_extract_tarball() {
local tag=$1
local target_dir=$2
if [ -z "$tag" ] || [ -z "$target_dir" ]; then
print_error "download_and_extract_tarball: Missing required parameters."
return 1
fi
local tarball_url="https://github.com/rcourtman/Pulse/releases/download/$tag/pulse-${tag#v}.tar.gz"
local temp_tarball="/tmp/pulse-$tag.tar.gz"
local temp_extract_dir="/tmp/pulse-extract-$$"
print_info "Downloading Pulse $tag tarball..."
if ! curl -sL "$tarball_url" -o "$temp_tarball"; then
print_error "Failed to download tarball from $tarball_url"
return 1
fi
print_info "Extracting tarball..."
mkdir -p "$temp_extract_dir"
if ! tar -xzf "$temp_tarball" -C "$temp_extract_dir" --strip-components=1; then
print_error "Failed to extract tarball"
rm -f "$temp_tarball"
rm -rf "$temp_extract_dir"
return 1
fi
# Create target directory if it doesn't exist
if [ ! -d "$target_dir" ]; then
mkdir -p "$target_dir"
chown "$PULSE_USER":"$PULSE_USER" "$target_dir"
fi
# Move extracted content to target directory
print_info "Installing files to $target_dir..."
# Enable dotglob to include hidden files like .env.example
shopt -s dotglob
if ! cp -r "$temp_extract_dir"/* "$target_dir/"; then
print_error "Failed to copy files to target directory"
shopt -u dotglob
rm -f "$temp_tarball"
rm -rf "$temp_extract_dir"
return 1
fi
shopt -u dotglob
# Set proper ownership
chown -R "$PULSE_USER":"$PULSE_USER" "$target_dir"
# Cleanup
rm -f "$temp_tarball"
rm -rf "$temp_extract_dir"
print_success "Tarball extracted and installed successfully."
return 0
}
perform_tarball_update() {
if [ -z "$TARGET_TAG" ]; then
print_error "Target version tag not determined. Cannot update via tarball."
return 1
fi
print_info "Attempting to update Pulse to version $TARGET_TAG using tarball..."
# Check if release exists before proceeding
if ! check_release_exists "$TARGET_TAG"; then
print_warning "Release tarball not available for $TARGET_TAG, falling back to git..."
return 1
fi
# Create comprehensive backup of user data
local user_data_backup=""
if [ -d "$PULSE_DIR" ]; then
cd "$PULSE_DIR" || { print_error "Failed to cd into $PULSE_DIR"; return 1; }
if user_data_backup=$(backup_user_data); then
print_success "User data backup created for tarball update"
else
print_warning "Failed to create user data backup, proceeding anyway..."
fi
cd ..
fi
local script_backup_path="/tmp/${SCRIPT_NAME}.bak"
if [ -n "$SCRIPT_ABS_PATH" ] && [ -f "$SCRIPT_ABS_PATH" ]; then
print_info "Backing up current installer script to $script_backup_path..."
if cp "$SCRIPT_ABS_PATH" "$script_backup_path"; then
print_success "Installer script backed up."
else
print_warning "Failed to back up installer script."
script_backup_path=""
fi
else
script_backup_path=""
fi
# Backup current installation directory (full backup as fallback)
local backup_dir="/tmp/pulse-backup-$(date +%s)"
if [ -d "$PULSE_DIR" ]; then
print_info "Creating full installation backup as safety measure..."
if ! cp -r "$PULSE_DIR" "$backup_dir"; then
print_warning "Failed to backup current installation, continuing anyway..."
fi
fi
# Download and extract new version
if download_and_extract_tarball "$TARGET_TAG" "$PULSE_DIR"; then
cd "$PULSE_DIR" || { print_error "Failed to cd into $PULSE_DIR"; return 1; }
# Restore installer script if we backed it up
if [ -n "$script_backup_path" ] && [ -f "$script_backup_path" ]; then
print_info "Restoring potentially updated installer script from backup..."
if cp "$script_backup_path" "$PULSE_DIR/scripts/$SCRIPT_NAME"; then
print_success "Installer script restored."
fi
rm -f "$script_backup_path"
fi
# The tarball should already include built assets, but ensure permissions are correct
print_info "Setting permissions for $PULSE_DIR..."
chown -R "$PULSE_USER":"$PULSE_USER" "$PULSE_DIR"
chmod +x "$PULSE_DIR/scripts/$SCRIPT_NAME" 2>/dev/null || true
# Check if node_modules exists in tarball, if not, install dependencies
if [ ! -d "$PULSE_DIR/node_modules" ]; then
print_info "Installing NPM dependencies in $PULSE_DIR (root project directory)..."
if npm install --omit=dev --silent >/dev/null 2>&1; then
print_success "NPM dependencies installed successfully in $PULSE_DIR."
else
print_error "Failed to install NPM dependencies in $PULSE_DIR."
if [ -d "$backup_dir" ]; then
print_info "Restoring from backup..."
rm -rf "$PULSE_DIR"
mv "$backup_dir" "$PULSE_DIR"
fi
return 1
fi
else
print_info "NPM dependencies already present in tarball."
fi
# Always check if CSS assets are valid after tarball extraction
print_info "Verifying CSS assets in $PULSE_DIR..."
# Check if output.css exists and is valid (not corrupted with HTML content)
if [ ! -f "$PULSE_DIR/src/public/output.css" ]; then
print_error "Critical: output.css file is missing after tarball extraction. This indicates a problem with the tarball or extraction process."
# Try to re-extract just the CSS file as a fallback
if curl -sL "https://github.com/rcourtman/Pulse/releases/download/$TARGET_TAG/pulse-${TARGET_TAG#v}.tar.gz" | tar -xzf - --to-stdout "pulse-${TARGET_TAG#v}/src/public/output.css" > "$PULSE_DIR/src/public/output.css" 2>/dev/null; then
print_success "CSS file recovered using direct extraction."
else
print_error "Failed to recover CSS file. Frontend may not display correctly."
fi
elif [ ! -s "$PULSE_DIR/src/public/output.css" ]; then
print_error "Critical: output.css file is empty after tarball extraction."
# Try to re-extract just the CSS file as a fallback
if curl -sL "https://github.com/rcourtman/Pulse/releases/download/$TARGET_TAG/pulse-${TARGET_TAG#v}.tar.gz" | tar -xzf - --to-stdout "pulse-${TARGET_TAG#v}/src/public/output.css" > "$PULSE_DIR/src/public/output.css" 2>/dev/null; then
print_success "CSS file recovered using direct extraction."
else
print_error "Failed to recover CSS file. Frontend may not display correctly."
fi
elif head -1 "$PULSE_DIR/src/public/output.css" 2>/dev/null | grep -q "<!DOCTYPE\|<html\|<head"; then
print_warning "output.css contains HTML instead of CSS - attempting to recover from tarball..."
# Try to re-extract just the CSS file as a fallback
if curl -sL "https://github.com/rcourtman/Pulse/releases/download/$TARGET_TAG/pulse-${TARGET_TAG#v}.tar.gz" | tar -xzf - --to-stdout "pulse-${TARGET_TAG#v}/src/public/output.css" > "$PULSE_DIR/src/public/output.css" 2>/dev/null; then
# Verify the recovery worked
if [ -s "$PULSE_DIR/src/public/output.css" ] && ! head -1 "$PULSE_DIR/src/public/output.css" 2>/dev/null | grep -q "<!DOCTYPE\|<html\|<head"; then
print_success "CSS file recovered and verified."
chown "$PULSE_USER":"$PULSE_USER" "$PULSE_DIR/src/public/output.css"
else
print_error "CSS recovery failed. Frontend may not display correctly."
fi
else
print_error "Failed to recover CSS file. Frontend may not display correctly."
fi
else
print_success "CSS assets verified and ready."
fi
# Restore user data after successful tarball extraction
if [ -n "$user_data_backup" ] && [ -d "$user_data_backup" ]; then
if restore_user_data "$user_data_backup"; then
print_success "User configuration restored after tarball update"
else
print_warning "Failed to restore some user configuration"
fi
rm -rf "$user_data_backup"
fi
# Cleanup backup if successful
[ -d "$backup_dir" ] && rm -rf "$backup_dir"
print_success "Tarball update completed successfully."
return 0
else
print_error "Failed to download/extract tarball."
# Restore backup if available
if [ -d "$backup_dir" ]; then
print_info "Restoring from full backup..."
rm -rf "$PULSE_DIR"
mv "$backup_dir" "$PULSE_DIR"
fi
# Cleanup failed backups
[ -n "$script_backup_path" ] && rm -f "$script_backup_path"
[ -n "$user_data_backup" ] && rm -rf "$user_data_backup"
return 1
fi
}
# Emergency recovery function for when updates fail
emergency_recovery() {
local backup_dir="$1"
print_error "==================== EMERGENCY RECOVERY ===================="
print_error "Update failed catastrophically. Attempting emergency recovery..."
if [ -n "$backup_dir" ] && [ -d "$backup_dir" ]; then
print_info "Restoring from emergency backup: $backup_dir"
if [ -d "$PULSE_DIR" ]; then
rm -rf "$PULSE_DIR.failed" 2>/dev/null
mv "$PULSE_DIR" "$PULSE_DIR.failed" 2>/dev/null
fi
if mv "$backup_dir" "$PULSE_DIR"; then
print_success "Emergency recovery successful!"
print_info "Your original configuration has been restored."
print_info "Failed installation moved to: $PULSE_DIR.failed"
return 0
else
print_error "Emergency recovery failed!"
fi
fi
print_error "============== MANUAL RECOVERY REQUIRED ================"
print_error "Automatic recovery failed. You may need to:"
print_error "1. Manually restore from backup if available"
print_error "2. Re-run the installer: sudo bash install-pulse.sh"
print_error "3. Reconfigure your .env file with Proxmox credentials"
print_error "4. Contact support: https://github.com/rcourtman/Pulse/issues"
print_error "======================================================="
return 1
}
perform_tarball_install() {
if [ -z "$TARGET_TAG" ]; then
print_error "Target version tag not determined. Cannot install via tarball."
return 1
fi
print_info "Installing Pulse $TARGET_TAG using tarball..."
# Check if release exists before proceeding
if ! check_release_exists "$TARGET_TAG"; then
print_warning "Release tarball not available for $TARGET_TAG, falling back to git..."
return 1
fi
# Download and extract
if download_and_extract_tarball "$TARGET_TAG" "$PULSE_DIR"; then
cd "$PULSE_DIR" || { print_error "Failed to cd into $PULSE_DIR"; return 1; }
# The tarball should already include built assets, but check if we need to install dependencies
if [ ! -d "$PULSE_DIR/node_modules" ]; then
print_info "Installing NPM dependencies in $PULSE_DIR (root project directory)..."
if npm install --omit=dev --silent >/dev/null 2>&1; then
print_success "NPM dependencies installed successfully in $PULSE_DIR."
else
print_error "Failed to install NPM dependencies in $PULSE_DIR."
return 1
fi
else
print_info "NPM dependencies already present in tarball."
fi
# Always check if CSS assets need building for tarball installs
print_info "Verifying CSS assets in $PULSE_DIR..."
# Check if output.css exists and is valid (not corrupted with HTML content)
css_needs_fix=false
if [ ! -f "$PULSE_DIR/src/public/output.css" ]; then
print_warning "output.css file is missing - should be present in tarball"
css_needs_fix=true
elif [ ! -s "$PULSE_DIR/src/public/output.css" ]; then
print_warning "output.css file is empty - should contain CSS from tarball"
css_needs_fix=true
elif head -1 "$PULSE_DIR/src/public/output.css" 2>/dev/null | grep -q "<!DOCTYPE\|<html\|<head"; then
print_warning "output.css contains HTML instead of CSS (this should not happen in fresh tarball)"
css_needs_fix=true
fi
if [ "$css_needs_fix" = "true" ]; then
print_error "CSS assets in tarball are invalid. This indicates a problem with the release tarball."
return 1
else
print_info "CSS assets verified and ready."
fi
print_success "Tarball installation completed successfully."
return 0
else
print_error "Failed to install from tarball."
return 1
fi
}
prompt_for_branch_selection() {
print_info "Fetching available feature branches..."
local branches
if [ -d "$PULSE_DIR" ]; then
cd "$PULSE_DIR" || return 1
branches=$(sudo -u "$PULSE_USER" git ls-remote --heads origin | { grep -E 'feature/|hotfix/|dev/' || true; } | sed 's|.*refs/heads/||' | sort)
cd - >/dev/null
else
# For fresh installations, fetch from remote
branches=$(git ls-remote --heads https://github.com/rcourtman/Pulse.git | { grep -E 'feature/|hotfix/|dev/' || true; } | sed 's|.*refs/heads/||' | sort)
fi
if [ -z "$branches" ]; then
print_warning "No development branches found."
return 1
fi
echo "Available development branches:"
local i=1
local branch_array=()
while IFS= read -r branch; do
echo " $i) $branch"
branch_array+=("$branch")
i=$((i+1))
done <<< "$branches"
echo " $i) Cancel"
read -p "Select a branch to install [1-$i]: " choice
if [ "$choice" = "$i" ]; then
return 1
elif [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then
SPECIFIED_BRANCH="${branch_array[$((choice-1))]}"
return 0
else
print_error "Invalid choice."
return 1
fi
}
check_installation_status_and_determine_action() {
if [ -n "$MODE_UPDATE" ]; then
print_info "Running in non-interactive update mode..."
INSTALL_MODE="update"
if [ -n "$SPECIFIED_VERSION_TAG" ]; then
if [ -d "$PULSE_DIR/.git" ]; then
cd "$PULSE_DIR" || { print_error "Failed to cd into $PULSE_DIR"; INSTALL_MODE="error"; return; }
if check_remote_tag_exists "$SPECIFIED_VERSION_TAG"; then
TARGET_TAG="$SPECIFIED_VERSION_TAG"
else
INSTALL_MODE="error"
cd ..; return
fi
cd ..
else
print_error "Cannot validate specified version tag: Pulse directory $PULSE_DIR not found."
INSTALL_MODE="error"; return
fi
elif [ -n "$SPECIFIED_BRANCH" ]; then
if [ -d "$PULSE_DIR/.git" ]; then
cd "$PULSE_DIR" || { print_error "Failed to cd into $PULSE_DIR"; INSTALL_MODE="error"; return; }
print_info "Checking if remote branch '$SPECIFIED_BRANCH' exists..."
if sudo -u "$PULSE_USER" git ls-remote --heads origin "$SPECIFIED_BRANCH" | grep -q "$SPECIFIED_BRANCH" 2>/dev/null; then
TARGET_BRANCH="$SPECIFIED_BRANCH"
print_info "Will update to branch: $TARGET_BRANCH"
else
print_error "Remote branch '$SPECIFIED_BRANCH' not found."
INSTALL_MODE="error"
cd ..; return
fi
cd ..
else
print_error "Cannot validate specified branch: Pulse directory $PULSE_DIR not found."
INSTALL_MODE="error"; return
fi
else
if [ -d "$PULSE_DIR/.git" ]; then
cd "$PULSE_DIR" || { print_error "Failed to cd into $PULSE_DIR"; INSTALL_MODE="error"; return; }
TARGET_TAG=$(get_latest_remote_tag)
if [ $? -ne 0 ] || [ -z "$TARGET_TAG" ]; then
print_error "Could not determine latest remote tag for update."
INSTALL_MODE="error"
fi
cd ..
else
print_info "Pulse directory $PULSE_DIR not found. Switching to installation mode."
INSTALL_MODE="install"
# Determine latest tag for fresh installation
TARGET_TAG=$(git ls-remote --tags --sort='-version:refname' https://github.com/rcourtman/Pulse.git | grep 'refs/tags/v' | head -n 1 | sed 's/.*refs\/tags\///' | sed 's/\^{}.*//')
if [ -z "$TARGET_TAG" ]; then
print_error "Could not determine latest release tag for installation."
INSTALL_MODE="error"; return
fi
print_info "Will install latest version: $TARGET_TAG"
fi
fi
return 0
fi
print_info "Checking Pulse installation at $PULSE_DIR..."
if [ -d "$PULSE_DIR" ]; then
# Check if this is a valid Pulse installation (either git or tarball)
if [ -f "$PULSE_DIR/package.json" ] && grep -q '"name".*"pulse"' "$PULSE_DIR/package.json" 2>/dev/null; then
print_info "Found valid Pulse installation"
# Determine installation type
local is_git_install=false
if [ -d "$PULSE_DIR/.git" ]; then
is_git_install=true
print_info "Installation type: Git repository"
else
print_info "Installation type: Tarball installation"
fi
else
print_error "Directory $PULSE_DIR exists but does not appear to be a valid Pulse installation."
print_error "Missing or invalid package.json file."
print_error "Please remove this directory manually ($PULSE_DIR) or choose a different installation path and re-run the script."
INSTALL_MODE="error"
return
fi
# Handle git installations
if [ "$is_git_install" = true ]; then
cd "$PULSE_DIR" || { print_error "Failed to cd into $PULSE_DIR"; INSTALL_MODE="error"; return; }
local current_tag
current_tag=$(get_current_local_tag)
local latest_tag
latest_tag=$(get_latest_remote_tag)
if [ $? -ne 0 ] || [ -z "$latest_tag" ]; then
print_warning "Could not determine the latest remote release tag. Cannot check for updates reliably."
INSTALL_MODE="update"
else
print_info "Current installed version tag: ${current_tag:-Not on a tag}"
print_info "Latest available version tag: $latest_tag"
# Check for specific version/branch requests first
if [ -n "$SPECIFIED_VERSION_TAG" ]; then
if check_remote_tag_exists "$SPECIFIED_VERSION_TAG"; then
TARGET_TAG="$SPECIFIED_VERSION_TAG"
print_info "Will target specified version: $TARGET_TAG"
INSTALL_MODE="update"
else
INSTALL_MODE="error"; cd ..; return
fi
elif [ -n "$SPECIFIED_BRANCH" ]; then
print_info "Checking if remote branch '$SPECIFIED_BRANCH' exists..."
if sudo -u "$PULSE_USER" git ls-remote --heads origin "$SPECIFIED_BRANCH" | grep -q "$SPECIFIED_BRANCH" 2>/dev/null; then
TARGET_BRANCH="$SPECIFIED_BRANCH"
print_info "Will switch to branch: $TARGET_BRANCH"
INSTALL_MODE="update"
else
print_error "Remote branch '$SPECIFIED_BRANCH' not found."
INSTALL_MODE="error"; cd ..; return
fi
elif [ -n "$current_tag" ] && [ "$current_tag" = "$latest_tag" ]; then
print_info "Pulse is already installed and up-to-date with the latest release $latest_tag."
INSTALL_MODE="uptodate"
TARGET_TAG="$latest_tag"
else
print_warning "Pulse is installed, but an update to $latest_tag is available."
INSTALL_MODE="update"
TARGET_TAG="$latest_tag"
fi
fi
if [ -n "$SPECIFIED_VERSION_TAG" ]; then
if check_remote_tag_exists "$SPECIFIED_VERSION_TAG"; then
TARGET_TAG="$SPECIFIED_VERSION_TAG"
print_info "Will target specified version: $TARGET_TAG"
INSTALL_MODE="update"
else
INSTALL_MODE="error"; cd ..; return
fi
elif [ -n "$SPECIFIED_BRANCH" ]; then
print_info "Checking if remote branch '$SPECIFIED_BRANCH' exists..."
if sudo -u "$PULSE_USER" git ls-remote --heads origin "$SPECIFIED_BRANCH" | grep -q "$SPECIFIED_BRANCH" 2>/dev/null; then
TARGET_BRANCH="$SPECIFIED_BRANCH"
print_info "Will switch to branch: $TARGET_BRANCH"
INSTALL_MODE="update"
else
print_error "Remote branch '$SPECIFIED_BRANCH' not found."
INSTALL_MODE="error"; cd ..; return
fi
else
TARGET_TAG="$latest_tag"
fi
cd ..
if [ "$INSTALL_MODE" = "uptodate" ]; then
if [ -n "$MODE_UPDATE" ]; then
# In non-interactive mode, skip if up to date
print_info "Pulse is already up-to-date. Skipping update."
INSTALL_MODE="cancel"
elif [ -n "$SPECIFIED_VERSION_TAG" ] && [ "$SPECIFIED_VERSION_TAG" != "$current_tag" ]; then
print_info "You requested version $SPECIFIED_VERSION_TAG, but $current_tag is installed."
echo -e "Choose an action:"
echo -e " 1) Manage automatic updates"
echo -e " 2) Re-install current version $current_tag"
echo -e " 3) Remove Pulse"
echo -e " 4) Cancel"
read -p "Enter your choice [1-4]: " user_choice
case $user_choice in
1) prompt_for_cron_setup; INSTALL_MODE="cancel" ;;
2) INSTALL_MODE="update" ;;
3) INSTALL_MODE="remove" ;;
4) INSTALL_MODE="cancel" ;;
*) print_error "Invalid choice."; INSTALL_MODE="error" ;;
esac
elif [ -n "$SPECIFIED_BRANCH" ]; then
if [ -n "$MODE_UPDATE" ]; then
# In non-interactive mode, proceed with branch update
INSTALL_MODE="update"
else
echo -e "Choose an action:"
echo -e " 1) Install branch $SPECIFIED_BRANCH (for testing)"
echo -e " 2) Remove Pulse"
echo -e " 3) Cancel"
echo -e " 4) Manage automatic updates"
read -p "Enter your choice [1-4]: " user_choice
case $user_choice in
1) INSTALL_MODE="update" ;;
2) INSTALL_MODE="remove" ;;
3) INSTALL_MODE="cancel" ;;
4) prompt_for_cron_setup; INSTALL_MODE="cancel" ;;
*) print_error "Invalid choice."; INSTALL_MODE="error" ;;
esac
fi
else
if [ -n "$MODE_UPDATE" ]; then
# In non-interactive mode, re-install if explicitly requested
INSTALL_MODE="update"
else
echo -e "Choose an action:"
echo -e " 1) Manage automatic updates"
echo -e " 2) Re-install current version $current_tag"
echo -e " 3) Test a feature branch"
echo -e " 4) Remove Pulse"
echo -e " 5) Cancel"
read -p "Enter your choice [1-5]: " user_choice
case $user_choice in
1) prompt_for_cron_setup; INSTALL_MODE="cancel" ;;
2) INSTALL_MODE="update" ;;
3)
if prompt_for_branch_selection; then
INSTALL_MODE="update"
else
INSTALL_MODE="cancel"
fi
;;
4) INSTALL_MODE="remove" ;;
5) INSTALL_MODE="cancel" ;;
*) print_error "Invalid choice."; INSTALL_MODE="error" ;;
esac
fi
fi
elif [ "$INSTALL_MODE" = "update" ]; then
if [ -n "$MODE_UPDATE" ]; then
# In non-interactive mode, proceed with update
: # Do nothing, keep INSTALL_MODE as "update"
elif [ -n "$SPECIFIED_VERSION_TAG" ]; then
echo "Choose an action:"
echo " 1) Install specified version $SPECIFIED_VERSION_TAG"
echo " 2) Remove Pulse"
echo " 3) Cancel"
echo " 4) Manage automatic updates"
read -p "Enter your choice [1-4]: " user_choice
case $user_choice in
1) INSTALL_MODE="update" ;;
2) INSTALL_MODE="remove" ;;
3) INSTALL_MODE="cancel" ;;
4) prompt_for_cron_setup; INSTALL_MODE="cancel" ;;
*) print_error "Invalid choice."; INSTALL_MODE="error" ;;
esac
elif [ -n "$SPECIFIED_BRANCH" ]; then
echo "Choose an action:"
echo " 1) Install branch $SPECIFIED_BRANCH (for testing)"
echo " 2) Remove Pulse"
echo " 3) Cancel"
echo " 4) Manage automatic updates"
read -p "Enter your choice [1-4]: " user_choice
case $user_choice in
1) INSTALL_MODE="update" ;;
2) INSTALL_MODE="remove" ;;
3) INSTALL_MODE="cancel" ;;
4) prompt_for_cron_setup; INSTALL_MODE="cancel" ;;
*) print_error "Invalid choice."; INSTALL_MODE="error" ;;
esac
else
echo "Choose an action:"
echo " 1) Update Pulse to the latest version $TARGET_TAG"
echo " 2) Test a feature branch"
echo " 3) Remove Pulse"
echo " 4) Cancel"
echo " 5) Manage automatic updates"
read -p "Enter your choice [1-5]: " user_choice
case $user_choice in
1) INSTALL_MODE="update" ;;
2)
if prompt_for_branch_selection; then
INSTALL_MODE="update"
else
INSTALL_MODE="cancel"
fi
;;
3) INSTALL_MODE="remove" ;;
4) INSTALL_MODE="cancel" ;;
5) prompt_for_cron_setup; INSTALL_MODE="cancel" ;;
*) print_error "Invalid choice."; INSTALL_MODE="error" ;;
esac
fi
fi
else
# Handle tarball installations
print_info "Handling tarball installation..."
# Get current version from package.json
local current_version=""
if [ -f "$PULSE_DIR/package.json" ]; then
current_version=$(grep '"version"' "$PULSE_DIR/package.json" | sed 's/.*"version": *"\([^"]*\)".*/\1/')
print_info "Current installed version: $current_version"
fi
# Determine latest version for comparison
local latest_tag=""
latest_tag=$(git ls-remote --tags --sort='-version:refname' https://github.com/rcourtman/Pulse.git | grep 'refs/tags/v' | head -n 1 | sed 's/.*refs\/tags\///' | sed 's/\^{}.*//')
if [ -n "$latest_tag" ]; then
local latest_version=$(echo "$latest_tag" | sed 's/^v//')
print_info "Latest available version: $latest_version"
# Compare versions
if [ -n "$SPECIFIED_VERSION_TAG" ]; then
TARGET_TAG="$SPECIFIED_VERSION_TAG"
print_info "Will target specified version: $TARGET_TAG"
INSTALL_MODE="update"
elif [ -n "$SPECIFIED_BRANCH" ]; then
TARGET_BRANCH="$SPECIFIED_BRANCH"
print_info "Will switch to branch: $TARGET_BRANCH"
INSTALL_MODE="update"
elif [ "$current_version" = "$latest_version" ]; then
print_info "Pulse is already up-to-date with the latest release $latest_tag."
INSTALL_MODE="uptodate"
TARGET_TAG="$latest_tag"
else
print_warning "Pulse is installed, but an update to $latest_tag is available."
INSTALL_MODE="update"
TARGET_TAG="$latest_tag"
fi
else
print_warning "Could not determine latest version. Will proceed with update."
INSTALL_MODE="update"
fi
# Handle user interaction for tarball installations
if [ "$INSTALL_MODE" = "uptodate" ]; then
if [ -n "$MODE_UPDATE" ]; then
print_info "Pulse is already up-to-date. Skipping update."
INSTALL_MODE="cancel"
else
echo "Choose an action:"
echo " 1) Manage automatic updates"
echo " 2) Re-install current version"
echo " 3) Test a feature branch"
echo " 4) Remove Pulse"
echo " 5) Cancel"
read -p "Enter your choice [1-5]: " user_choice
case $user_choice in
1) prompt_for_cron_setup; INSTALL_MODE="cancel" ;;
2) INSTALL_MODE="update" ;;
3)
if prompt_for_branch_selection; then
INSTALL_MODE="update"
else
INSTALL_MODE="cancel"
fi
;;
4) INSTALL_MODE="remove" ;;
5) INSTALL_MODE="cancel" ;;
*) print_error "Invalid choice."; INSTALL_MODE="error" ;;
esac
fi
elif [ "$INSTALL_MODE" = "update" ]; then
if [ -n "$MODE_UPDATE" ]; then
# In non-interactive mode, proceed with update
: # Do nothing, keep INSTALL_MODE as "update"
else
echo "Choose an action:"
echo " 1) Update Pulse to the latest version ${TARGET_TAG:-$latest_tag}"
echo " 2) Test a feature branch"
echo " 3) Remove Pulse"
echo " 4) Cancel"
echo " 5) Manage automatic updates"
read -p "Enter your choice [1-5]: " user_choice
case $user_choice in
1) INSTALL_MODE="update" ;;
2)
if prompt_for_branch_selection; then
INSTALL_MODE="update"
else
INSTALL_MODE="cancel"
fi
;;
3) INSTALL_MODE="remove" ;;
4) INSTALL_MODE="cancel" ;;
5) prompt_for_cron_setup; INSTALL_MODE="cancel" ;;
*) print_error "Invalid choice."; INSTALL_MODE="error" ;;
esac
fi
fi
fi
else
print_info "Pulse is not currently installed."
if [ -n "$SPECIFIED_VERSION_TAG" ]; then
TARGET_TAG="$SPECIFIED_VERSION_TAG"
print_info "Will attempt to install specified version: $TARGET_TAG"
INSTALL_MODE="install"
elif [ -n "$SPECIFIED_BRANCH" ]; then
TARGET_BRANCH="$SPECIFIED_BRANCH"
print_info "Will attempt to install from branch: $TARGET_BRANCH"
INSTALL_MODE="install"
else
if [ -n "$MODE_UPDATE" ]; then
# In non-interactive mode, install latest version
INSTALL_MODE="install"
else
echo "Choose an action:"
echo " 1) Install Pulse [latest version]"
echo " 2) Test a feature branch"
echo " 3) Cancel"
read -p "Enter your choice [1-3]: " user_choice
case $user_choice in
1) INSTALL_MODE="install" ;;
2)
if prompt_for_branch_selection; then
INSTALL_MODE="install"
else
INSTALL_MODE="cancel"
fi
;;
3) INSTALL_MODE="cancel" ;;
*) print_error "Invalid choice."; INSTALL_MODE="error" ;;
esac
fi
fi
fi
}
run_apt_update() {
print_info "Updating package lists..."
if apt-get update -qq > /dev/null; then
print_success "Package lists updated."
else
print_error "Failed to update package lists."
return 1
fi
return 0
}
run_apt_upgrade_system() {
print_info "Upgrading system packages (this may take a while)..."
if apt-get upgrade -y -qq > /dev/null; then
print_success "System packages upgraded."
else
print_error "Failed to upgrade system packages."
return 1
fi
return 0
}
install_dependencies() {
print_info "Checking and installing necessary dependencies (git, curl, sudo, gpg, diffutils)..."
local required_deps=("git" "curl" "sudo" "gpg" "diffutils")
local missing_deps=()
local dep_to_install
for dep in "${required_deps[@]}"; do
if ! command -v "$dep" &> /dev/null; then
missing_deps+=("$dep")
fi
done
if [ ${#missing_deps[@]} -gt 0 ]; then
print_info "Missing dependencies: ${missing_deps[*]}. Attempting to install..."
dep_to_install=$(IFS=","; echo "${missing_deps[*]}") # For print message
if apt-get install -y -qq "${missing_deps[@]}" > /dev/null; then
print_success "Successfully installed: ${dep_to_install}."
else
print_error "Failed to install: ${dep_to_install}."
exit 1
fi
else
print_info "All core dependencies are already installed."
fi
print_success "Dependency check complete."
}
setup_node() {
print_info "Setting up Node.js repository [NodeSource]..."
if command -v node &> /dev/null; then
current_node_version=$(node -v 2>/dev/null)
current_major_version=$(echo "$current_node_version" | sed 's/v//' | cut -d. -f1)
if [[ -n "$current_major_version" ]] && [[ "$current_major_version" -ge "$NODE_MAJOR_VERSION" ]]; then
print_info "Node.js version ${current_node_version} already installed and meets requirement >= v${NODE_MAJOR_VERSION}.x. Skipping setup."
return 0
else
print_warning "Installed Node.js version $current_node_version does not meet requirement >= v${NODE_MAJOR_VERSION}.x or could not be determined. Proceeding with setup..."
fi
else
print_info "Node.js not found. Proceeding with setup..."
fi
if ! command -v curl &> /dev/null; then
print_error "curl is required but not found. Please install it first [apt-get install curl]."
exit 1
fi
KEYRING_DIR="/usr/share/keyrings"
KEYRING_FILE="$KEYRING_DIR/nodesource.gpg"
if [ ! -d "$KEYRING_DIR" ]; then
mkdir -p "$KEYRING_DIR" || { print_error "Failed to create $KEYRING_DIR"; exit 1; }
fi
print_info "Downloading/refreshing NodeSource GPG key..."
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --yes --dearmor -o "$KEYRING_FILE"
if [ $? -ne 0 ]; then
print_error "Failed to download or process NodeSource GPG key."
rm -f "$KEYRING_FILE"
exit 1
fi
echo "deb [signed-by=$KEYRING_FILE] https://deb.nodesource.com/node_$NODE_MAJOR_VERSION.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null
if [ $? -ne 0 ]; then
print_error "Failed to add NodeSource repository to sources list."
exit 1
fi
print_info "Updating package list after adding NodeSource repository..."
if ! apt-get update > /dev/null; then
print_error "Failed to update package list after adding NodeSource repository."
rm -f /etc/apt/sources.list.d/nodesource.list "$KEYRING_FILE"
exit 1
fi
print_info "Installing Node.js ${NODE_MAJOR_VERSION}.x..."
if apt-get install nodejs -y > /dev/null; then
print_success "Node.js ${NODE_MAJOR_VERSION}.x installed successfully."
print_info "Node version: $(node -v)"
print_info "npm version: $(npm -v)"
else
print_error "Failed to install Node.js."
rm -f /etc/apt/sources.list.d/nodesource.list "$KEYRING_FILE"
exit 1
fi
}
create_pulse_user() {
print_info "Creating dedicated user '$PULSE_USER'..."
if id "$PULSE_USER" &>/dev/null; then
print_warning "User '$PULSE_USER' already exists. Skipping creation."
else
useradd -r -s /bin/false "$PULSE_USER"
if [ $? -eq 0 ]; then
print_success "User '$PULSE_USER' created successfully."
else
print_error "Failed to create user '$PULSE_USER'."
exit 1
fi
fi
}
# Comprehensive backup of user configuration and data
backup_user_data() {
local backup_base_dir="/tmp/pulse-config-backup-$(date +%s)"
mkdir -p "$backup_base_dir" || {
print_error "Failed to create backup directory $backup_base_dir"
return 1
}
print_info "Creating comprehensive backup of user configuration..."
# Files to always preserve
local preserve_files=(
".env"
"custom-config.json"
"user-settings.json"
)
# Directories to preserve (if they exist and contain user data)
local preserve_dirs=(
"data"
"logs"
"backups"
"custom"
)
local backup_success=false
# Backup individual files
for file in "${preserve_files[@]}"; do
if [ -f "$PULSE_DIR/$file" ]; then
local file_backup_dir="$backup_base_dir/$(dirname "$file")"
mkdir -p "$file_backup_dir"
if cp "$PULSE_DIR/$file" "$backup_base_dir/$file" 2>/dev/null; then
print_success "Backed up: $file"
backup_success=true
else
print_warning "Failed to backup: $file"
fi
fi
done
# Backup directories
for dir in "${preserve_dirs[@]}"; do
if [ -d "$PULSE_DIR/$dir" ] && [ "$(ls -A "$PULSE_DIR/$dir" 2>/dev/null)" ]; then
if cp -r "$PULSE_DIR/$dir" "$backup_base_dir/$dir" 2>/dev/null; then
print_success "Backed up directory: $dir"
backup_success=true
else
print_warning "Failed to backup directory: $dir"
fi
fi
done
if [ "$backup_success" = true ]; then
echo "$backup_base_dir"
return 0
else
rm -rf "$backup_base_dir" 2>/dev/null
print_warning "No user data found to backup or all backups failed"
return 1
fi
}
# Restore user configuration and data
restore_user_data() {
local backup_dir="$1"
if [ -z "$backup_dir" ] || [ ! -d "$backup_dir" ]; then
print_warning "No backup directory provided or directory doesn't exist"
return 1
fi
print_info "Restoring user configuration from backup..."
# Restore files and directories
if [ -d "$backup_dir" ]; then
if cp -r "$backup_dir"/* "$PULSE_DIR/" 2>/dev/null; then
# Fix ownership and permissions
chown -R "$PULSE_USER":"$PULSE_USER" "$PULSE_DIR"
# Set secure permissions on sensitive files
[ -f "$PULSE_DIR/.env" ] && chmod 600 "$PULSE_DIR/.env"
print_success "User configuration restored successfully"
return 0
else
print_error "Failed to restore user configuration"
return 1
fi
fi
return 1
}
perform_update() {
if [ -z "$TARGET_TAG" ] && [ -z "$TARGET_BRANCH" ]; then
print_error "Target version tag or branch not determined. Cannot update."
return 1
fi
# For version tags, try tarball first, then fall back to git
if [ -n "$TARGET_TAG" ]; then
print_info "Attempting to update Pulse to version $TARGET_TAG..."
# Try tarball update first - this is safer and preserves user data
if perform_tarball_update; then
return 0
fi
# Fall back to git update only if tarball fails
print_warning "Tarball update failed, falling back to git update..."
print_warning "⚠️ Git update may be more disruptive to user configuration"
else
print_info "Attempting to update Pulse to branch $TARGET_BRANCH..."
print_warning "⚠️ Branch installations are for testing only and may be unstable!"
fi
cd "$PULSE_DIR" || { print_error "Failed to change directory to $PULSE_DIR"; return 1; }
# Create comprehensive backup of all user data before ANY destructive operations
local user_data_backup=""
if user_data_backup=$(backup_user_data); then
print_success "User data backup created at: $user_data_backup"
else
print_warning "Failed to create comprehensive user data backup"
print_warning "Proceeding with update but configuration may be lost"
fi
local script_backup_path="/tmp/${SCRIPT_NAME}.bak"
if [ -n "$SCRIPT_ABS_PATH" ] && [ -f "$SCRIPT_ABS_PATH" ]; then
print_info "Backing up current installer script to $script_backup_path..."
if cp "$SCRIPT_ABS_PATH" "$script_backup_path"; then
print_success "Installer script backed up."
else
print_warning "Failed to back up installer script. Update might revert it."
script_backup_path=""
fi
else
print_warning "Cannot determine current installer script path. Cannot back it up."
script_backup_path=""
fi
git config --global --add safe.directory "$PULSE_DIR" > /dev/null 2>&1 || print_warning "Could not configure safe.directory for root user."
# Check git status before destructive operations
local has_local_changes=false
if sudo -u "$PULSE_USER" git status --porcelain 2>/dev/null | grep -q .; then
has_local_changes=true
print_warning "Local changes detected in repository"
fi
print_info "Resetting local repository to discard potential changes [running as user $PULSE_USER]..."
if ! sudo -u "$PULSE_USER" git reset --hard HEAD; then
print_error "Failed to reset local repository. Aborting update."
[ -n "$script_backup_path" ] && rm -f "$script_backup_path"
[ -n "$user_data_backup" ] && rm -rf "$user_data_backup"
cd ..
return 1
fi
# Only clean untracked files if absolutely necessary
# Skip git clean entirely and handle conflicts more gracefully
if [ "$has_local_changes" = true ]; then
print_info "Checking for untracked files that might conflict..."
local untracked_files=$(sudo -u "$PULSE_USER" git status --porcelain 2>/dev/null | grep '^??' | cut -c4-)
if [ -n "$untracked_files" ]; then
print_warning "Found untracked files that may conflict with update:"
echo "$untracked_files" | while read -r file; do
print_warning " - $file"
done
# Instead of git clean, selectively handle conflicts
print_info "Moving conflicting files to temporary backup..."
local conflict_backup="/tmp/pulse-conflicts-$(date +%s)"
mkdir -p "$conflict_backup"
echo "$untracked_files" | while read -r file; do
if [ -f "$file" ] || [ -d "$file" ]; then
local file_dir="$conflict_backup/$(dirname "$file")"
mkdir -p "$file_dir"
mv "$file" "$conflict_backup/$file" 2>/dev/null || true
fi
done
fi
fi
print_info "Fetching latest changes from git [running as user $PULSE_USER]..."
if [ -n "$TARGET_TAG" ]; then
if ! sudo -u "$PULSE_USER" git fetch origin --tags --force; then
print_error "Failed to fetch latest changes/tags from git."
[ -n "$script_backup_path" ] && rm -f "$script_backup_path"
cd ..
return 1
fi
if ! sudo -u "$PULSE_USER" git rev-parse "$TARGET_TAG" >/dev/null 2>&1; then
print_error "Target tag '$TARGET_TAG' could not be found locally after fetch."
print_error "It might have been deleted remotely or there was a fetch issue."
[ -n "$script_backup_path" ] && rm -f "$script_backup_path"
cd ..
return 1
fi
print_info "Checking out target version tag '$TARGET_TAG'..."
if ! sudo -u "$PULSE_USER" git checkout -f "$TARGET_TAG"; then
print_error "Failed to checkout tag '$TARGET_TAG'."
[ -n "$script_backup_path" ] && rm -f "$script_backup_path"
cd ..
return 1
fi
print_info "Verifying package.json was updated..."
local package_version=$(node -p "require('./package.json').version" 2>/dev/null || echo "unknown")
local expected_version=$(echo "$TARGET_TAG" | sed 's/^v//')
if [ "$package_version" != "$expected_version" ] && [ "$package_version" != "unknown" ]; then
print_warning "package.json version ($package_version) does not match expected version ($expected_version)"
print_info "Forcing checkout to ensure all files are updated..."
sudo -u "$PULSE_USER" git checkout -f "$TARGET_TAG" -- .
fi
else
# Branch checkout
if ! sudo -u "$PULSE_USER" git fetch origin "$TARGET_BRANCH"; then
print_error "Failed to fetch branch '$TARGET_BRANCH' from git."
[ -n "$script_backup_path" ] && rm -f "$script_backup_path"
cd ..
return 1
fi
print_info "Checking out branch '$TARGET_BRANCH'..."
if ! sudo -u "$PULSE_USER" git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH"; then
print_error "Failed to checkout branch '$TARGET_BRANCH'."
[ -n "$script_backup_path" ] && rm -f "$script_backup_path"
cd ..
return 1
fi
print_info "Pulling latest changes from branch..."
if ! sudo -u "$PULSE_USER" git pull origin "$TARGET_BRANCH"; then
print_warning "Failed to pull latest changes, continuing with current state"
fi
fi
local current_tag
current_tag=$(sudo -u "$PULSE_USER" git describe --tags --exact-match HEAD 2>/dev/null)
if [ -n "$script_backup_path" ] && [ -f "$script_backup_path" ]; then
print_info "Restoring potentially updated installer script from backup..."
if cp "$script_backup_path" "$SCRIPT_ABS_PATH"; then
print_success "Installer script restored."
else
print_warning "Failed to restore installer script from backup."
print_warning "The version in $SCRIPT_ABS_PATH might be outdated."
fi
rm -f "$script_backup_path"
fi
if [ -n "$SCRIPT_ABS_PATH" ] && [ -f "$SCRIPT_ABS_PATH" ]; then
print_info "Ensuring install script $SCRIPT_ABS_PATH is executable..."
if chmod +x "$SCRIPT_ABS_PATH"; then
print_success "Install script executable permission set."
else
print_warning "Failed to set executable permission on install script."
fi
else
print_warning "Could not find script path $SCRIPT_ABS_PATH to ensure executable permission."
fi
print_info "Installing NPM dependencies in $PULSE_DIR (root project directory)..."
if npm install --unsafe-perm --silent; then
print_success "NPM dependencies installed successfully in $PULSE_DIR."
else
print_error "Failed to install NPM dependencies in $PULSE_DIR. See npm output above."
exit 1
fi
print_info "Building CSS assets in $PULSE_DIR..."
if npm run build:css --silent; then
print_success "CSS assets built successfully."
else
print_error "Failed to build CSS assets. See npm output above."
print_warning "Continuing update, but frontend may not display correctly."
fi
# Restore user data BEFORE setting permissions
if [ -n "$user_data_backup" ] && [ -d "$user_data_backup" ]; then
if restore_user_data "$user_data_backup"; then
print_success "User configuration and data restored successfully"
else
print_warning "Failed to restore some user configuration"
fi
# Cleanup backup after restoration
rm -rf "$user_data_backup"
fi
set_permissions
print_info "Ensuring systemd service $SERVICE_NAME is configured..."
if ! setup_systemd_service true; then
print_error "Failed to configure systemd service during update."
return 1
fi
print_info "===> Attempting to restart Pulse service $SERVICE_NAME..."
systemctl restart "$SERVICE_NAME"
local restart_exit_code=$?
if [ $restart_exit_code -eq 0 ]; then
print_success "Pulse service restart command finished successfully [Exit code: $restart_exit_code]."
else
print_error "Pulse service restart command failed [Exit code: $restart_exit_code]."
print_warning "Please check the service status manually: sudo systemctl status $SERVICE_NAME"
print_warning "And check logs: sudo journalctl -u $SERVICE_NAME"
return 1
fi
if [ -n "$TARGET_TAG" ]; then
print_info "Performing post-update version verification..."
local installed_version=$(node -p "require('./package.json').version" 2>/dev/null || echo "unknown")
local expected_version=$(echo "$TARGET_TAG" | sed 's/^v//')
if [ "$installed_version" = "$expected_version" ]; then
print_success "Version verification passed: $installed_version"
elif [ "$installed_version" = "unknown" ]; then
print_warning "Could not verify installed version from package.json"
else
print_warning "Version mismatch detected!"
print_warning "Expected: $expected_version, Found: $installed_version"
print_warning "The application may not report the correct version."
fi
if [ -n "$current_tag" ]; then
print_success "Pulse updated successfully to version $current_tag!"
else
print_success "Pulse updated successfully! [Could not confirm exact tag]"
fi
print_info "The application should now report version: $expected_version"
else
# Branch update
local current_branch=$(sudo -u "$PULSE_USER" git rev-parse --abbrev-ref HEAD 2>/dev/null)
local current_commit=$(sudo -u "$PULSE_USER" git rev-parse --short HEAD 2>/dev/null)
print_success "Pulse updated successfully to branch: $current_branch!"
if [ -n "$current_commit" ]; then
print_info "Current commit: $current_commit"
fi
print_warning "⚠️ Running from branch - version reporting may show development version"
fi
return 0
}
perform_remove() {
print_warning "This will stop and disable the Pulse service[s] and remove the installation directory ($PULSE_DIR)."
local remove_confirm=""
if [ -t 0 ]; then
read -p "Are you sure you want to remove Pulse? [y/N]: " remove_confirm
if [[ ! "$remove_confirm" =~ ^[Yy]$ ]]; then
print_info "Removal cancelled."
return 1
fi
else
print_warning "Running non-interactively. Proceeding with removal..."
fi
local potential_services=("pulse-monitor.service" "pulse-proxmox.service")
local service_removed=false
for service_name in "${potential_services[@]}"; do
local service_file_path="/etc/systemd/system/$service_name"
if systemctl list-units --full -all | grep -q "$service_name" 2>/dev/null; then
print_info "Stopping service $service_name..."
systemctl stop "$service_name" > /dev/null 2>&1
print_info "Disabling service $service_name..."
systemctl disable "$service_name" > /dev/null 2>&1
if [ -f "$service_file_path" ]; then
print_info "Removing systemd service file $service_file_path..."
rm -f "$service_file_path"
if [ $? -eq 0 ]; then
print_success "Service file $service_file_path removed."
service_removed=true
else
print_warning "Failed to remove service file $service_file_path. Please remove it manually."
fi
else
print_info "Service file $service_file_path not found, skipping removal."
fi
else
print_info "Service $service_name not found, skipping stop/disable."
if [ -f "$service_file_path" ]; then
print_info "Removing orphaned systemd service file $service_file_path..."
rm -f "$service_file_path"
if [ $? -eq 0 ]; then
print_success "Orphaned service file $service_file_path removed."
service_removed=true
else
print_warning "Failed to remove orphaned service file $service_file_path. Please remove it manually."
fi
fi
fi
done
if [ "$service_removed" = true ]; then
print_info "Reloading systemd daemon..."
systemctl daemon-reload
fi
print_info "Removing Pulse installation directory $PULSE_DIR..."
if rm -rf "$PULSE_DIR"; then
print_success "Installation directory removed."
else
print_error "Failed to remove installation directory $PULSE_DIR. Please remove it manually."
return 1
fi
print_success "Pulse removed successfully."
return 0
}
install_npm_deps() {
print_info "Installing npm dependencies..."
if [ ! -d "$PULSE_DIR" ]; then
print_error "Pulse directory $PULSE_DIR not found. Cannot install dependencies."
return 1
fi
cd "$PULSE_DIR" || { print_error "Failed to change directory to $PULSE_DIR"; return 1; }
print_info "Installing root dependencies [including dev]..."
if npm install --unsafe-perm > /dev/null 2>&1; then
print_success "Root dependencies installed."
else
print_error "Failed to install root npm dependencies."
return 1
fi
print_info "Installing server dependencies [including dev]..."
cd server || { print_error "Failed to change directory to $PULSE_DIR/server"; cd ..; return 1; }
if npm install --unsafe-perm > /dev/null 2>&1; then
print_success "Server dependencies installed."
else
print_error "Failed to install server npm dependencies."
cd ..
return 1
fi
cd ..
return 0
}
set_permissions() {
print_info "Setting permissions for $PULSE_DIR..."
if chown -R "$PULSE_USER":"$PULSE_USER" "$PULSE_DIR"; then
print_success "Permissions set correctly."
return 0
else
print_error "Failed to set permissions for $PULSE_DIR."
print_warning "Check ownership and permissions for $PULSE_DIR."
return 1
fi
}
configure_environment() {
print_info "Configuring Pulse environment..."
local env_example_path="$PULSE_DIR/.env.example"
local env_path="$PULSE_DIR/.env"
if [ ! -f "$env_example_path" ]; then
print_error "Environment example file not found at $env_example_path. Cannot configure."
return 1
fi
if [ -f "$env_path" ]; then
print_warning "Configuration file $env_path already exists."
if [ -t 0 ]; then
read -p "Overwrite existing configuration? [y/N]: " overwrite_confirm
if [[ ! "$overwrite_confirm" =~ ^[Yy]$ ]]; then
print_info "Skipping environment configuration."
return 0
fi
print_info "Proceeding to overwrite existing configuration..."
else
print_info "Running non-interactively, skipping environment configuration as file exists."
return 0
fi
fi
local proxmox_host=""
local proxmox_token_id=""
local proxmox_token_secret=""
local allow_self_signed="y"
local pulse_port=""
if [ -t 0 ]; then
echo "Please provide your Proxmox connection details:"
read -p " -> Proxmox Host URL [e.g., https://192.168.1.100:8006]: " proxmox_host
while [ -z "$proxmox_host" ]; do
print_warning "Proxmox Host URL cannot be empty."
read -p " -> Proxmox Host URL [e.g., https://192.168.1.100:8006]: " proxmox_host
done
if [[ ! "$proxmox_host" =~ ^https?:// ]]; then
print_warning "URL does not start with http:// or https://. Prepending https://."
proxmox_host="https://$proxmox_host"
print_info "Using Proxmox Host URL: $proxmox_host"
fi
echo ""
print_info "You need a Proxmox API Token. You can create one via the Proxmox Web UI,"
print_info "or run the following commands on your Proxmox host shell:"
echo "----------------------------------------------------------------------"
echo " # 1. Create user 'pulse-monitor' [enter password when prompted]:"
echo " pveum useradd pulse-monitor@pam -comment "API user for Pulse monitoring""
echo ' '
echo " # 2. Create API token 'pulse' for user [COPY THE SECRET VALUE!]:"
echo " pveum user token add pulse-monitor@pam pulse --privsep=1"
echo ' '
echo " # 3. Assign PVEAuditor role to user:"
echo " pveum acl modify / -user pulse-monitor@pam -role PVEAuditor"
echo "----------------------------------------------------------------------"
echo "After running the 'token add' command, copy the Token ID and Secret Value"
echo "and paste them below."
echo ""
read -p " -> Proxmox API Token ID [e.g., user@pam!tokenid]: " proxmox_token_id
while [ -z "$proxmox_token_id" ]; do
print_warning "Proxmox Token ID cannot be empty."
read -p " -> Proxmox API Token ID [e.g., user@pam!tokenid]: " proxmox_token_id
done
read -sp " -> Proxmox API Token Secret: " proxmox_token_secret
echo
while [ -z "$proxmox_token_secret" ]; do
print_warning "Proxmox Token Secret cannot be empty."
read -sp " -> Proxmox API Token Secret: " proxmox_token_secret
echo
done
read -p "Allow self-signed certificates for Proxmox? [Y/n]: " allow_self_signed
read -p "Port for Pulse server [leave blank for default 7655]: " pulse_port
else
print_warning "Running non-interactively. Cannot prompt for environment details."
print_warning "Ensure $env_path is configured manually or exists from a previous run."
if [ -f "$env_path" ]; then return 0; fi
fi
local self_signed_value="true"
if [[ "$allow_self_signed" =~ ^[Nn]$ ]]; then self_signed_value="false"; fi
local port_value="7655"
if [ -n "$pulse_port" ]; then
if [[ "$pulse_port" =~ ^[0-9]+$ ]] && [ "$pulse_port" -ge 1 ] && [ "$pulse_port" -le 65535 ]; then
port_value="$pulse_port"
else
print_warning "Invalid port number entered. Using default 7655."
fi
fi
print_info "Creating $env_path from example..."
if cp "$env_example_path" "$env_path"; then
if [ -n "$proxmox_host" ]; then
sed -i "s|^PROXMOX_HOST=.*|PROXMOX_HOST=$proxmox_host|" "$env_path"
sed -i "s|^PROXMOX_TOKEN_ID=.*|PROXMOX_TOKEN_ID=$proxmox_token_id|" "$env_path"
sed -i "s|^PROXMOX_TOKEN_SECRET=.*|PROXMOX_TOKEN_SECRET=$proxmox_token_secret|" "$env_path"
sed -i "s|^PROXMOX_ALLOW_SELF_SIGNED_CERTS=.*|PROXMOX_ALLOW_SELF_SIGNED_CERTS=$self_signed_value|" "$env_path"
sed -i "s|^PORT=.*|PORT=$port_value|" "$env_path"
else
print_warning "Skipping variable substitution in .env as no values were provided [non-interactive?]."
print_warning "Please edit $env_path manually."
fi
chown "$PULSE_USER":"$PULSE_USER" "$env_path"
chmod 600 "$env_path"
print_success "Environment file created/updated in $env_path."
return 0
else
print_error "Failed to copy $env_example_path to $env_path."
return 1
fi
}
setup_systemd_service() {
local update_mode=${1:-false}
print_info "Setting up systemd service $SERVICE_NAME..."
local service_file="/etc/systemd/system/$SERVICE_NAME"
local node_path
node_path=$(command -v node)
if [ -z "$node_path" ]; then
print_error "Could not find Node.js executable path. Cannot create service."
return 1
fi
local npm_path
npm_path=$(command -v npm)
if [ -z "$npm_path" ]; then
print_warning "Could not find npm executable path. Service might require adjustment."
local node_dir
node_dir=$(dirname "$node_path")
npm_path="$node_dir/npm"
if ! command -v "$npm_path" &> /dev/null; then
print_error "Could not reliably find npm executable path. Cannot create service."
return 1
fi
print_info "Found npm at $npm_path"
fi
print_info "Creating/Updating service file at $service_file..."
cat << EOF > "$service_file"
[Unit]
Description=Pulse for Proxmox VE Monitoring Application
After=network.target
[Service]
Type=simple
User=$PULSE_USER
Group=$PULSE_USER
WorkingDirectory=$PULSE_DIR
EnvironmentFile=$PULSE_DIR/.env
ExecStart=$node_path $npm_path run start
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
if [ $? -ne 0 ]; then
print_error "Failed to create systemd service file $service_file."
return 1
fi
chmod 644 "$service_file"
print_info "Reloading systemd daemon..."
systemctl daemon-reload
if [ "$update_mode" = true ]; then
print_info "[Update mode: Skipping service enable/start]"
return 0
fi
print_info "Enabling $SERVICE_NAME to start on boot..."
if systemctl enable "$SERVICE_NAME" > /dev/null 2>&1; then
print_success "Service enabled successfully."
else
print_error "Failed to enable systemd service."
return 1
fi
print_info "Starting $SERVICE_NAME..."
if systemctl start "$SERVICE_NAME"; then
print_success "Service started successfully."
else
print_error "Failed to start systemd service."
print_warning "Please check the service status using: systemctl status $SERVICE_NAME"
print_warning "And check the logs using: journalctl -u $SERVICE_NAME"
return 1
fi
return 0
}
disable_cron_update() {
print_info "Disabling Pulse automatic update cron job..."
local cron_identifier="# Pulse-Auto-Update [$SCRIPT_NAME]"
current_cron=$(crontab -l -u root 2>/dev/null || true)
local identifier_for_grep_check
identifier_for_grep_check=$(echo "$cron_identifier" | sed 's/[]$.*^[]/\\&/g')
if ! echo "$current_cron" | grep -q "^${identifier_for_grep_check}$"; then
print_info "Pulse automatic update cron job not found. Nothing to disable."
return 0
fi
local escaped_identifier_for_awk
escaped_identifier_for_awk=$(echo "$cron_identifier" | sed 's/[][\\.*^$(){}|+?]/\\\\&/g')
local awk_script='/__PATTERN__/ { skip=1; next } skip == 1 { skip=0; next } { print }'
awk_script="${awk_script/__PATTERN__/^$escaped_identifier_for_awk$}"
filtered_cron=$(echo "$current_cron" | awk "$awk_script")
trimmed_filtered_cron=$(echo "$filtered_cron" | awk 'NF')
if [ -z "$trimmed_filtered_cron" ]; then
echo "" | crontab -u root -
else
echo "$trimmed_filtered_cron" | crontab -u root -
fi
if [ $? -eq 0 ]; then
print_success "Cron job for automatic updates disabled successfully."
else
print_error "Failed to disable cron job. Please check crontab configuration manually."
return 1
fi
return 0
}
setup_cron_update() {
local cron_schedule=""
local cron_command=""
local script_path="$SCRIPT_ABS_PATH"
local cron_identifier="# Pulse-Auto-Update [$SCRIPT_NAME]"
if [ -z "$script_path" ] || [ ! -f "$script_path" ]; then
print_warning "Could not reliably determine script path for cron job. Skipping auto-update setup."
return 1
fi
print_info "Choose update frequency [will update to the *latest release tag*]:"
echo " 1) Daily"
echo " 2) Weekly"
echo " 3) Monthly"
echo " 4) Never [Cancel]"
read -p "Enter your choice [1-4]: " freq_choice
case $freq_choice in
1) cron_schedule="@daily" ;;
2) cron_schedule="@weekly" ;;
3) cron_schedule="@monthly" ;;
4) print_info "Automatic updates setup cancelled."; return 0 ;;
*) print_error "Invalid choice. Skipping auto-update setup."; return 1 ;;
esac
cron_command="$cron_schedule /usr/bin/bash $script_path --update >> $LOG_FILE 2>&1"
print_info "Checking/Updating cron job for Pulse automatic updates..."
current_cron=$(crontab -l -u root 2>/dev/null || true)
local escaped_identifier_for_awk
escaped_identifier_for_awk=$(echo "$cron_identifier" | sed 's/[][\\.*^$(){}|+?]/\\\\&/g')
local awk_script='/__PATTERN__/ { skip=1; next } skip == 1 { skip=0; next } { print }'
awk_script="${awk_script/__PATTERN__/^$escaped_identifier_for_awk$}"
filtered_cron=$(echo "$current_cron" | awk "$awk_script")
local new_cron_content
trimmed_filtered_cron=$(echo "$filtered_cron" | awk 'NF')
if [ -n "$trimmed_filtered_cron" ]; then
new_cron_content=$(printf "%s\n%s\n%s" "$trimmed_filtered_cron" "$cron_identifier" "$cron_command")
else
new_cron_content=$(printf "%s\n%s" "$cron_identifier" "$cron_command")
fi
echo "$new_cron_content" | crontab -u root -
if [ $? -eq 0 ]; then
print_success "Cron job for automatic updates set successfully."
print_info "Update logs will be written to $LOG_FILE"
touch "$LOG_FILE" || print_warning "Could not touch log file $LOG_FILE"
chown root:root "$LOG_FILE" || print_warning "Could not chown log file $LOG_FILE"
chmod 644 "$LOG_FILE" || print_warning "Could not chmod log file $LOG_FILE"
else
print_error "Failed to update cron job. Please check crontab configuration manually."
return 1
fi
return 0
}
prompt_for_cron_setup() {
if [ -n "$MODE_UPDATE" ]; then
return 0
fi
local cron_exists=false
local cron_identifier="# Pulse-Auto-Update [$SCRIPT_NAME]"
local identifier_for_grep_check
identifier_for_grep_check=$(echo "$cron_identifier" | sed 's/[]$.*^[]/\\&/g')
if crontab -l -u root 2>/dev/null | grep -q "^${identifier_for_grep_check}$"; then
cron_exists=true
fi
echo ""
if [ "$cron_exists" = true ]; then
print_info "Automatic updates for Pulse appear to be currently ENABLED."
print_info "[Cron job will update to the latest release tag when run]"
echo -e "Choose an action:"
echo -e " 1) Keep current schedule [Do nothing]"
echo -e " 2) Disable automatic updates"
echo -e " 3) Change update schedule"
read -p "Enter your choice [1-3]: " cron_manage_choice
case $cron_manage_choice in
1) print_info "Keeping current automatic update schedule.";;
2) disable_cron_update ;;
3) setup_cron_update ;;
*) print_warning "Invalid choice. No changes made to automatic updates.";;
esac
else
print_info "Automatic updates for Pulse appear to be currently DISABLED."
print_info "[Cron job would update to the latest release tag when run]"
read -p "Do you want to set up automatic updates for Pulse? [Y/n]: " setup_cron_confirm
if [[ ! "$setup_cron_confirm" =~ ^[Nn]$ ]]; then
setup_cron_update
else
print_info "Skipping automatic update setup."
fi
fi
}
final_instructions() {
local ip_address
ip_address=$(hostname -I | awk '{print $1}')
local port_value
local env_path="$PULSE_DIR/.env"
if [ -f "$env_path" ] && grep -q '^PORT=' "$env_path"; then
port_value=$(grep '^PORT=' "$env_path" | cut -d'=' -f2)
else
port_value="7655"
fi
local final_tag=""
local final_branch=""
local final_commit=""
if [ -d "$PULSE_DIR/.git" ]; then
cd "$PULSE_DIR" || print_warning "Could not cd to $PULSE_DIR to get version info."
final_tag=$(get_current_local_tag)
if [ -z "$final_tag" ]; then
final_branch=$(sudo -u "$PULSE_USER" git rev-parse --abbrev-ref HEAD 2>/dev/null)
final_commit=$(sudo -u "$PULSE_USER" git rev-parse --short HEAD 2>/dev/null)
fi
cd ..
fi
echo ""
print_success "Pulse for Proxmox VE installation/update complete!"
if [ -n "$final_tag" ]; then
print_success "Current version installed: $final_tag"
elif [ -n "$final_branch" ]; then
print_success "Running from branch: $final_branch"
if [ -n "$final_commit" ]; then
print_info "Current commit: $final_commit"
fi
print_warning "⚠️ This is a test/development branch - not for production use!"
fi
echo "-------------------------------------------------------------"
print_info "You should be able to access the Pulse dashboard at:"
if [ -n "$ip_address" ]; then
echo " http://$ip_address:$port_value"
else
echo " http://<YOUR-LXC-IP>:$port_value"
print_warning "Could not automatically determine the LXC IP address."
fi
echo ""
print_info "The Pulse service $SERVICE_NAME is running and enabled on boot."
print_info "To check the status: sudo systemctl status $SERVICE_NAME"
print_info "To view logs: sudo journalctl -u $SERVICE_NAME -f"
echo "-------------------------------------------------------------"
}
check_root
if [ -z "$MODE_UPDATE" ] && [ "$INSTALLER_WAS_REEXECUTED" != "true" ]; then
print_dependency_info
fi
self_update_check || print_warning "Installer self-check failed, proceeding anyway..."
check_installation_status_and_determine_action
case "$INSTALL_MODE" in
"remove")
print_info "Proceeding with removal..."
perform_remove
exit $?
;;
"cancel")
print_info "Operation cancelled by user."
exit 0
;;
"error")
print_error "Exiting due to error."
exit 1
;;
"install" | "update")
print_info "Proceeding with install/update. Installing prerequisites..."
if [ "$INSTALL_MODE" = "install" ]; then
run_apt_update || exit 1
run_apt_upgrade_system || exit 1
elif [ "$INSTALL_MODE" = "update" ]; then
run_apt_update || exit 1
fi
install_dependencies || exit 1
setup_node || exit 1
create_pulse_user || exit 1
print_success "Prerequisites installed."
if [ "$INSTALL_MODE" = "install" ]; then
print_info "Starting installation..."
if [ -n "$SPECIFIED_VERSION_TAG" ]; then
if ! git ls-remote --tags --exit-code origin "refs/tags/$SPECIFIED_VERSION_TAG"; then
print_error "Specified version tag '$SPECIFIED_VERSION_TAG' not found on remote repository."
exit 1
else
print_info "Specified version tag '$SPECIFIED_VERSION_TAG' confirmed."
TARGET_TAG="$SPECIFIED_VERSION_TAG"
fi
fi
# For branch installations, use git clone
if [ -n "$TARGET_BRANCH" ]; then
print_info "Cloning Pulse repository into $PULSE_DIR for branch installation..."
if git clone https://github.com/rcourtman/Pulse.git "$PULSE_DIR" > /dev/null 2>&1; then
print_success "Repository cloned successfully."
cd "$PULSE_DIR" || { print_error "Failed to cd into $PULSE_DIR after clone."; exit 1; }
chown -R "$PULSE_USER":"$PULSE_USER" "$PULSE_DIR" || print_warning "Failed initial chown after clone."
print_info "Checking out branch '$TARGET_BRANCH'..."
if ! sudo -u "$PULSE_USER" git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH"; then
print_error "Failed to checkout branch '$TARGET_BRANCH' after cloning."
cd ..; exit 1
fi
print_success "Checked out branch $TARGET_BRANCH."
print_warning "⚠️ Branch installation is for testing only and may be unstable!"
else
print_error "Failed to clone repository."
exit 1
fi
else
# For version tag installations, try tarball first
if [ -z "$TARGET_TAG" ]; then
# Need to determine latest tag - use git ls-remote for this
print_info "Determining latest release version..."
latest_tag=$(git ls-remote --tags --sort='-version:refname' https://github.com/rcourtman/Pulse.git | grep 'refs/tags/v' | head -n 1 | sed 's/.*refs\/tags\///' | sed 's/\^{}.*//')
if [ -z "$latest_tag" ]; then
print_error "Could not determine latest release tag to install."
exit 1
fi
TARGET_TAG="$latest_tag"
print_info "Determined latest version tag: $TARGET_TAG"
fi
# Try tarball installation first
TARBALL_INSTALL_SUCCESS=false
if perform_tarball_install; then
print_info "Installation completed using tarball."
TARBALL_INSTALL_SUCCESS=true
else
# Fall back to git clone
print_info "Tarball installation failed, falling back to git clone..."
print_info "Cloning Pulse repository into $PULSE_DIR..."
if git clone https://github.com/rcourtman/Pulse.git "$PULSE_DIR" > /dev/null 2>&1; then
print_success "Repository cloned successfully."
cd "$PULSE_DIR" || { print_error "Failed to cd into $PULSE_DIR after clone."; exit 1; }
chown -R "$PULSE_USER":"$PULSE_USER" "$PULSE_DIR" || print_warning "Failed initial chown after clone."
print_info "Checking out target version tag '$TARGET_TAG'..."
if ! sudo -u "$PULSE_USER" git checkout "$TARGET_TAG"; then
print_error "Failed to checkout tag '$TARGET_TAG' after cloning."
cd ..; exit 1
fi
print_success "Checked out version $TARGET_TAG."
else
print_error "Failed to clone repository."
exit 1
fi
fi
fi
cd ..
# Skip npm install and CSS build if tarball installation was successful
# (tarball should already include these)
if [ "$TARBALL_INSTALL_SUCCESS" != "true" ]; then
print_info "Installing NPM dependencies in $PULSE_DIR..."
cd "$PULSE_DIR" || { print_error "Failed to cd to $PULSE_DIR before npm install"; exit 1; }
if ! npm install --unsafe-perm --silent; then
print_error "Failed to install NPM dependencies. See output above."
exit 1
else
print_success "NPM dependencies installed."
fi
print_info "Building CSS assets in $PULSE_DIR..."
if ! npm run build:css --silent; then
print_error "Failed to build CSS assets. See output above."
exit 1
else
print_success "CSS assets built."
fi
else
# Ensure we're in the right directory for subsequent operations
cd "$PULSE_DIR" || { print_error "Failed to cd to $PULSE_DIR"; exit 1; }
print_info "Skipping npm install and CSS build (already included in tarball)."
fi
set_permissions || exit 1
configure_environment || exit 1
setup_systemd_service || exit 1
final_instructions
prompt_for_cron_setup
else
print_info "Starting update to version $TARGET_TAG..."
if perform_update; then
print_success "Pulse update completed successfully."
final_instructions
prompt_for_cron_setup
else
print_error "Pulse update failed."
exit 1
fi
fi
;;
*)
print_error "Internal script error: Unknown INSTALL_MODE '$INSTALL_MODE'"
exit 1
;;
esac
print_info "Script finished."