#!/bin/bash # Script Version: a29105e # Testing Self-Update Again: Another small change. # Pulse for Proxmox VE LXC Installation Script # This script automates the installation and setup of Pulse within a Proxmox LXC container. # --- Configuration --- NODE_MAJOR_VERSION=20 # Specify the desired Node.js major version [e.g., 18, 20] PULSE_DIR="/opt/pulse-proxmox" PULSE_USER="pulse" # Dedicated user to run Pulse SERVICE_NAME="pulse-monitor.service" SCRIPT_NAME="install-pulse.sh" # Used for cron job identification LOG_FILE="/var/log/pulse_update.log" # Log file for cron updates SCRIPT_ABS_PATH="" # Store absolute path of the script here 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" # Updated to match current commit # Version of the script being run (will be replaced by self-update mechanism) INSTALLER_VERSION="0.0.0" # Placeholder # --- Flags & Variables --- MODE_UPDATE="" # Flag to run in non-interactive update mode (empty string means false) INSTALL_MODE="" # Stores the determined action: install, update, remove, cancel, error SPECIFIED_VERSION_TAG="" # Stores tag specified via --version TARGET_TAG="" # Stores the final tag to be installed/updated INSTALLER_WAS_REEXECUTED=false # ---> NEW: Flag to track re-execution # Determine absolute path of the script early if command -v readlink &> /dev/null && readlink -f "$0" &> /dev/null; then SCRIPT_ABS_PATH=$(readlink -f "$0") else # Basic fallback if readlink -f is not available or fails if [[ "$0" == /* ]]; then SCRIPT_ABS_PATH="$0" else SCRIPT_ABS_PATH="$(pwd)/$0" fi # Verify the fallback path if [ ! -f "$SCRIPT_ABS_PATH" ]; then # Cannot reliably get path, self-update won't work safely SCRIPT_ABS_PATH="" fi fi # --- Argument Parsing --- # Consume arguments until --version is found, then expect its value while [[ "$#" -gt 0 ]]; do case $1 in --update) MODE_UPDATE="true"; shift ;; # Set to non-empty value --installer-reexecuted) INSTALLER_WAS_REEXECUTED=true shift # Consume the flag ;; --version) if [[ -n "$2" ]] && [[ "$2" != --* ]]; then SPECIFIED_VERSION_TAG="$2" shift 2 # Consume --version and its value else echo "Error: --version requires a tag name [e.g., v3.2.3]" >&2 exit 1 fi ;; *) echo "Unknown parameter passed: $1"; exit 1 ;; esac done # --- Helper Functions --- 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 } # ---> NEW: Function to inform about dependencies < --- print_dependency_info() { # --- ASCII Banner (from stash) --- echo -e "\033[1;34m" # Start color blue echo ' ____ _ ' echo '| _ \ _ _| |___ ___ ' echo '| |_) | | | | / __|/ _ \' echo '| __/| |_| | \__ \ __/' echo '|_| \__,_|_|___/\___|' echo ' ' echo -e "\033[0m" # End color echo "" # Keep blank line for spacing 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 "" # --- Display Dependencies (Using condensed version) --- 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" # --- End Display Dependencies --- # --- NEW: Confirmation Prompt (Default Yes) --- local confirm_proceed # Prompt defaults to Yes [Y/n] read -p "Do you want to proceed with the installation/update? [Y/n]: " confirm_proceed # Exit only if user explicitly enters 'n' or 'N' if [[ "$confirm_proceed" =~ ^[Nn]$ ]]; then print_error "Operation aborted by user." exit 1 # Use exit code 1 for aborted operations fi print_info "Proceeding..." # --- END Confirmation Prompt --- } # --- END NEW FUNCTION --- # --- Self-Update Function (GitHub API + jq + git) --- self_update_check() { # Only run check if interactive and script path was found if [ ! -t 0 ] || [ -n "$MODE_UPDATE" ] || [ -z "$SCRIPT_ABS_PATH" ]; then # Check if MODE_UPDATE is non-empty return 0 fi # --- Dependencies Check --- 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..." # Attempt to install jq quietly. Requires apt-get. if command -v apt-get &> /dev/null; then apt-get update -qq > /dev/null # Update package lists quietly 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 # Skip self-update if install failed 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 # Skip self-update if apt-get not found fi fi # Git is not strictly needed for the SHA comparison, but keep check for now 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 we are *inside* the cloned repo, git is needed for other parts later. # If not yet cloned, it's okay to not have it for this check. # We rely on the absolute script path existing for updates. 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 # --- END Dependencies Check --- print_info "Checking for updates to the installer script itself (using GitHub API)..." # --- Get Latest Commit SHA from GitHub API --- local owner="rcourtman" local repo="Pulse" local script_relative_path="scripts/install-pulse.sh" # Relative path within repo 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 # --- END Get Latest Commit SHA --- # --- Use Embedded SHA for Local Version --- # The currently running script's commit SHA is hardcoded at the top 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 # --- END Use Embedded SHA --- # --- Compare SHAs --- 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 # ---> NEW: Update embedded SHA in the downloaded file BEFORE fixing endings/moving < --- print_info "[DEBUG] Updating embedded SHA in temp script to $latest_remote_sha..." # ---> START REPLACEMENT LOGIC (Robust alternative to sed) local processed_temp_script="${temp_script}.processed" local line_updated=false while IFS= read -r line || [[ -n "$line" ]]; do # Check if the line starts with the variable we want to replace if [[ "$line" == CURRENT_SCRIPT_COMMIT_SHA=* ]]; then echo "CURRENT_SCRIPT_COMMIT_SHA=\"$latest_remote_sha\"" >> "$processed_temp_script" line_updated=true # Optional: Fix line endings (though curl might handle this) # elif [[ "$line" == *$'\r' ]]; then # echo "${line%$'\r'}" >> "$processed_temp_script" else # Write other lines as-is 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 # ---> END REPLACEMENT LOGIC <--- # ---> NOW work with processed_temp_script <--- 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 # --- Move the PROCESSED temp file into place --- 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" # Clean up both temps return 1 fi rm -f "$temp_script" # Clean up original temp file print_success "Installer updated successfully to commit ${latest_remote_sha:0:7}." print_info "Re-executing with updated installer..." # ---> MODIFY exec to pass flag < --- 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)" # Use info level return 0 fi } # --- Git Helper Functions --- # Fetches remote tags and returns the latest semantic version tag [vX.Y.Z] get_latest_remote_tag() { local latest_tag print_info "Fetching latest remote tags..." >&2 # Redirect info message to stderr # Use sudo only if not already root [though check_root ensures we are] if ! sudo -u "$PULSE_USER" git fetch origin --tags --force >/dev/null 2>&1; then print_warning "Could not fetch remote tags." >&2 # Also redirect warning return 1 fi # Sort tags semantically [version sort] and get the latest 'v*' tag 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 # Also redirect warning return 1 fi echo "$latest_tag" # This is the intended stdout return 0 } # Checks if a specific tag exists remotely 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..." # Use ls-remote to check efficiently 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 } # Gets the tag currently checked out get_current_local_tag() { local current_tag # describe might fail if not exactly on a tag, try rev-parse first on HEAD # then try describe. If HEAD isn't tagged, describe --tags might give nearest ancestor. current_tag=$(sudo -u "$PULSE_USER" git describe --tags --exact-match HEAD 2>/dev/null) if [ $? -ne 0 ]; then # Not exactly on a tag, maybe try nearest? Or just report empty? # For comparison, maybe nearest is okay, but indicates "not on latest release" current_tag=$(sudo -u "$PULSE_USER" git describe --tags --abbrev=0 HEAD 2>/dev/null) # If even that fails, return empty if [ $? -ne 0 ]; then current_tag="" fi fi echo "$current_tag" } # --- Installation Status Check & Action Determination --- check_installation_status_and_determine_action() { # Check if running in non-interactive update mode FIRST if [ -n "$MODE_UPDATE" ]; then # Check if MODE_UPDATE is non-empty print_info "Running in non-interactive update mode..." INSTALL_MODE="update" # Determine target tag [latest unless specified] if [ -n "$SPECIFIED_VERSION_TAG" ]; then # Need to check if repo exists to run git commands 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" # Tag not found cd ..; return fi cd .. else print_error "Cannot validate specified version tag: Pulse directory $PULSE_DIR not found." INSTALL_MODE="error"; return fi else # Need to check if repo exists to run git commands 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_error "Cannot determine latest version tag: Pulse directory $PULSE_DIR not found." INSTALL_MODE="error"; return fi fi # If error occurred determining tag, INSTALL_MODE will be set to error return 0 # Continue to main update logic if no error fi print_info "Checking Pulse installation at $PULSE_DIR..." if [ -d "$PULSE_DIR" ]; then # Directory exists if [ -d "$PULSE_DIR/.git" ]; then # It's a git repository cd "$PULSE_DIR" || { print_error "Failed to cd into $PULSE_DIR"; INSTALL_MODE="error"; return; } local current_tag current_tag=$(get_current_local_tag) # Get tag of current HEAD local latest_tag latest_tag=$(get_latest_remote_tag) # Get latest remote tag [also fetches] 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" # Offer update anyway else print_info "Current installed version tag: ${current_tag:-Not on a tag}" print_info "Latest available version tag: $latest_tag" if [ -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" else print_warning "Pulse is installed, but an update to $latest_tag is available." INSTALL_MODE="update" fi fi # Determine target tag for potential install/update 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" # Force update mode if specific version requested else INSTALL_MODE="error"; cd ..; return # Specified tag not found fi else TARGET_TAG="$latest_tag" # Default to latest determined tag fi cd .. # Go back to original directory # Prompt based on status if [ "$INSTALL_MODE" = "uptodate" ]; then # If user specified a version different from current, offer update if [ -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 # Updated range case $user_choice in 1) prompt_for_cron_setup; INSTALL_MODE="cancel" ;; # Call cron setup and then cancel main flow 2) INSTALL_MODE="update" ;; # Treat re-run as update 3) INSTALL_MODE="remove" ;; 4) INSTALL_MODE="cancel" ;; *) print_error "Invalid choice."; INSTALL_MODE="error" ;; esac else # Up to date and no specific version requested OR specified matches current 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 # Updated range case $user_choice in 1) prompt_for_cron_setup; INSTALL_MODE="cancel" ;; # Call cron setup and then cancel main flow 2) INSTALL_MODE="update" ;; # Treat re-run as update 3) INSTALL_MODE="remove" ;; 4) INSTALL_MODE="cancel" ;; *) print_error "Invalid choice."; INSTALL_MODE="error" ;; esac fi elif [ "$INSTALL_MODE" = "update" ]; then # Update available or fetch failed if [ -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 # Updated range case $user_choice in 1) INSTALL_MODE="update" ;; 2) INSTALL_MODE="remove" ;; 3) INSTALL_MODE="cancel" ;; 4) prompt_for_cron_setup; INSTALL_MODE="cancel" ;; # Call cron setup and then cancel main flow *) print_error "Invalid choice."; INSTALL_MODE="error" ;; esac else # Defaulting to latest tag echo "Choose an action:" echo " 1) Update Pulse to the latest version $TARGET_TAG" echo " 2) Remove Pulse" echo " 3) Cancel" echo " 4) Manage automatic updates" read -p "Enter your choice [1-4]: " user_choice # Updated range case $user_choice in 1) INSTALL_MODE="update" ;; 2) INSTALL_MODE="remove" ;; 3) INSTALL_MODE="cancel" ;; 4) prompt_for_cron_setup; INSTALL_MODE="cancel" ;; # Call cron setup and then cancel main flow *) print_error "Invalid choice."; INSTALL_MODE="error" ;; esac fi fi else # Directory exists but isn't a git repository print_error "Directory $PULSE_DIR exists but does not appear to be a valid Pulse git repository." print_error "Please remove this directory manually \($PULSE_DIR\) or choose a different installation path and re-run the script." INSTALL_MODE="error" fi else # Directory doesn't exist - Offer Install/Cancel print_info "Pulse is not currently installed." # Determine target tag for potential install if [ -n "$SPECIFIED_VERSION_TAG" ]; then # Need to check tag existence BEFORE offering install # Clone temporarily to check? No, use ls-remote. Need git installed first... # Assume git is installed by dependencies step later, check tag existence THEN. # For now, just store it. TARGET_TAG="$SPECIFIED_VERSION_TAG" print_info "Will attempt to install specified version: $TARGET_TAG" INSTALL_MODE="install" else # Need git to determine latest. We'll do this later in the install step. # Set mode first. echo "Choose an action:" echo " 1) Install Pulse [latest version]" echo " 2) Cancel" read -p "Enter your choice [1-2]: " user_choice case $user_choice in 1) INSTALL_MODE="install" ;; # Target tag determined later 2) INSTALL_MODE="cancel" ;; *) print_error "Invalid choice."; INSTALL_MODE="error" ;; esac fi fi # We don't return here, INSTALL_MODE is now set globally for the main logic } # --- System Setup Functions --- [apt_update_upgrade, install_dependencies, setup_node, create_pulse_user] apt_update_upgrade() { print_info "Updating package lists and upgrading packages..." if apt-get update > /dev/null && apt-get upgrade -y > /dev/null; then print_success "System packages updated and upgraded." else print_error "Failed to update/upgrade system packages." exit 1 fi } install_dependencies() { print_info "Installing necessary dependencies [git, curl, sudo, gpg, diffutils]..." # Added diffutils previously, keeping it if apt-get install -y git curl sudo gpg diffutils > /dev/null; then print_success "Dependencies installed." else print_error "Failed to install dependencies." exit 1 fi } setup_node() { print_info "Setting up Node.js repository [NodeSource]..." # Check if Node.js is already installed and meets version requirement 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 # Check if curl is installed before using it 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 # Add NodeSource repository GPG key and setup script 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" # Clean up partial file exit 1 fi # Add the repository configuration 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." # Attempt cleanup 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." # Attempt cleanup 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 # Create a system user with no login shell and no home directory [or specify one if needed] 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'." # Decide if this is fatal. Maybe allow script to continue if user exists? # For now, exiting as it might indicate a problem. exit 1 fi fi } # --- Core Action Functions --- [perform_update, perform_remove] perform_update() { # TARGET_TAG should be set by check_installation_status_and_determine_action if [ -z "$TARGET_TAG" ]; then print_error "Target version tag not determined. Cannot update." return 1 fi print_info "Attempting to update Pulse to version $TARGET_TAG..." cd "$PULSE_DIR" || { print_error "Failed to change directory to $PULSE_DIR"; return 1; } # ---> NEW: Backup the current installer script < --- 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." # Clear backup path so we don't try to restore/remove it later script_backup_path="" fi else print_warning "Cannot determine current installer script path. Cannot back it up." script_backup_path="" fi # --- END NEW STEP --- # Add safe directory config for root user, just in case git config --global --add safe.directory "$PULSE_DIR" > /dev/null 2>&1 || print_warning "Could not configure safe.directory for root user." # ---> Force reset local changes before fetching <--- print_info "Resetting local repository to discard potential changes [running as user $PULSE_USER]..." # This WILL revert the installer script in the workdir if it was updated by self_update if ! sudo -u "$PULSE_USER" git reset --hard HEAD; then print_error "Failed to reset local repository. Aborting update." # Clean up backup if it exists [ -n "$script_backup_path" ] && rm -f "$script_backup_path" cd .. return 1 fi # ---> End modification <--- print_info "Fetching latest changes and tags from git [running as user $PULSE_USER]..." # Ensure tags are fetched 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 # Check if target tag actually exists after fetching 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'..." # Checkout the specific tag. This will result in a detached HEAD state. if ! sudo -u "$PULSE_USER" git checkout "$TARGET_TAG"; then print_error "Failed to checkout tag '$TARGET_TAG'." [ -n "$script_backup_path" ] && rm -f "$script_backup_path" cd .. return 1 fi # Get the tag again now that we've checked it out local current_tag current_tag=$(sudo -u "$PULSE_USER" git describe --tags --exact-match HEAD 2>/dev/null) # Should now match TARGET_TAG print_info "Cleaning repository [removing untracked files]..." # Remove untracked files and directories to ensure a clean state # Use -f for files, -d for directories. Do NOT use -x which removes ignored files like .env if ! sudo -u "$PULSE_USER" git clean -fd; then print_warning "Failed to clean untracked files from the repository." # Continue anyway, as the core update [checkout] succeeded fi # ---> NEW: Restore the installer script if it was backed 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" "$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 # Clean up the backup file now rm -f "$script_backup_path" fi # --- END NEW STEP --- # Ensure the script itself remains executable after update/restore 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 "Re-installing npm dependencies [root]..." if ! npm install --unsafe-perm > /dev/null 2>&1; then print_warning "Failed to install root npm dependencies during update. Continuing..." else print_success "Root dependencies updated." fi print_info "Re-installing server dependencies..." 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 updated." else print_error "Failed to install server npm dependencies." exit 1 # Exit if server deps fail fi cd .. # Back to PULSE_DIR # Build CSS after dependencies print_info "Building CSS assets..." if ! npm run build:css > /dev/null 2>&1; then print_error "Failed to build CSS assets during update." print_warning "Continuing update despite CSS build failure." else print_success "CSS assets built." fi set_permissions # Ensure permissions are correct after update and build # Ensure the systemd service is configured correctly before restarting print_info "Ensuring systemd service $SERVICE_NAME is configured..." if ! setup_systemd_service true; then # Pass true to indicate update mode [skip start/enable] 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 # Use the actual checked-out tag in the success message 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 return 0 } perform_remove() { print_warning "This will stop and disable the Pulse service[s] and remove the installation directory ($PULSE_DIR)." # Allow non-interactive removal if called directly with logic before # For now, keep interactive confirmation here local remove_confirm="" if [ -t 0 ]; then # Check if stdin is a terminal for interactive prompt 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 # Indicate cancellation fi else print_warning "Running non-interactively. Proceeding with removal..." fi # List of potential service names to remove 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"; then print_info "Stopping service $service_name..." systemctl stop "$service_name" > /dev/null 2>&1 # Ignore errors if already stopped print_info "Disabling service $service_name..." systemctl disable "$service_name" > /dev/null 2>&1 # Ignore errors if already disabled 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." # Also check if the file exists even if service isn't loaded 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 # Reload systemd daemon if any service file was removed 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 # Indicate error fi print_success "Pulse removed successfully." return 0 } # --- Installation Step Functions --- [install_npm_deps, set_permissions, configure_environment, setup_systemd_service] install_npm_deps() { print_info "Installing npm dependencies..." if [ ! -d "$PULSE_DIR" ]; then print_error "Pulse directory $PULSE_DIR not found. Cannot install dependencies." # This shouldn't happen if called in the right flow, but check anyway return 1 fi cd "$PULSE_DIR" || { print_error "Failed to change directory to $PULSE_DIR"; return 1; } print_info "Installing root dependencies [including dev]..." # Use --unsafe-perm if running npm install as root, which might be necessary for some packages # Includes dev deps needed for build tools like postcss/autoprefixer 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 .. # Go back before returning error return 1 fi # Return to script execution directory or root, if needed 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." # This might not be fatal, but could cause runtime issues. Log warning? print_warning "Check ownership and permissions for $PULSE_DIR." return 1 # Indicate potential problem 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 # Check if .env already exists if [ -f "$env_path" ]; then print_warning "Configuration file $env_path already exists." # Only prompt if interactive 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 # Exit the function successfully without configuring fi print_info "Proceeding to overwrite existing configuration..." else print_info "Running non-interactively, skipping environment configuration as file exists." return 0 fi fi # --- Gather Proxmox Details [Only if interactive] --- 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 # Validate and potentially prepend https:// 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 # --- Display Token Generation Info --- 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 # --- Optional Settings --- 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." # Use defaults or skip creation if running non-interactively? # For now, just skip the interactive part and proceed to copy/sed if file doesn't exist # or if overwrite was forced [though non-interactive won't force] if [ -f "$env_path" ]; then return 0; fi # Skip if file exists and non-interactive fi # Determine values [use defaults if not set interactively] 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 # --- Create .env file --- [Handle case where variables might be empty if non-interactive] print_info "Creating $env_path from example..." if cp "$env_example_path" "$env_path"; then # Only run sed if variables were set [i.e., interactive mode ran] 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 # Set ownership & permissions regardless 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() { # Add optional argument to skip start/enable during updates local update_mode=${1:-false} # Default to false if no argument passed print_info "Setting up systemd service $SERVICE_NAME..." local service_file="/etc/systemd/system/$SERVICE_NAME" # Find Node path [needed for systemd ExecStart] 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 # Find npm path [needed for systemd ExecStart] 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." # Try to find it relative to node? Often in ../lib/node_modules/npm/bin/npm-cli.js local node_dir node_dir=$(dirname "$node_path") # This is a common structure, adjust if needed npm_path="$node_dir/npm" # Adjust if npm is installed elsewhere 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..." # Use a HEREDOC to write the service file contents 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 # Load environment variables from .env file in the working directory # Using explicit path instead of %W due to potential expansion issues EnvironmentFile=$PULSE_DIR/.env # Start command using absolute paths found earlier ExecStart=$node_path $npm_path run start # Restart policy Restart=on-failure RestartSec=5 # Environment [optional, if needed, but .env should handle this] # Environment="NODE_ENV=production" # Standard output/error logging 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 # Set permissions for the service file chmod 644 "$service_file" print_info "Reloading systemd daemon..." systemctl daemon-reload # Skip enable/start if in update mode 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." # Attempt cleanup of service file? # rm -f "$service_file" 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" # Don't necessarily exit, user might be able to fix it. return 1 # Indicate start failure fi return 0 } # --- Final Steps Functions --- [setup_cron_update, disable_cron_update, prompt_for_cron_setup, final_instructions] # [No changes needed in disable_cron_update or setup_cron_update logic, but prompt needs adjusting] # Function to specifically disable the cron job disable_cron_update() { print_info "Disabling Pulse automatic update cron job..." local cron_identifier="# Pulse-Auto-Update [$SCRIPT_NAME]" # Use the variable # Get current crontab content or empty string if none exists current_cron=$(crontab -l -u root 2>/dev/null || true) # Check if the job actually exists before trying to remove # Escape only for grep's basic regex needs here local identifier_for_grep_check identifier_for_grep_check=$(echo "$cron_identifier" | sed 's/[]$.*^[]/\\&/g') # Escape regex metachars for grep 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 # --- Use awk to remove existing blocks --- # Escape the identifier for awk pattern matching (ensure literal match) local escaped_identifier_for_awk # Escape for extended regex used by awk escaped_identifier_for_awk=$(echo "$cron_identifier" | sed 's/[][\\.*^$(){}|+?]/\\\\&/g') local awk_script='/__PATTERN__/ { skip=1; next } skip == 1 { skip=0; next } { print }' # Replace placeholder with escaped identifier, using ^ to anchor at start of line awk_script="${awk_script/__PATTERN__/^$escaped_identifier_for_awk$}" # Filter the crontab, preserving other lines filtered_cron=$(echo "$current_cron" | awk "$awk_script") # --- End awk filtering --- # Load the modified crontab content # Trim potential leading/trailing whitespace/newlines from filtered_cron trimmed_filtered_cron=$(echo "$filtered_cron" | awk 'NF') # Get non-empty lines if [ -z "$trimmed_filtered_cron" ]; then echo "" | crontab -u root - # Load empty crontab else echo "$trimmed_filtered_cron" | crontab -u root - # Load filtered content 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]" # Use the variable 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*]:" # Clarified update target 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 # Construct the cron command cron_command="$cron_schedule /usr/bin/bash $script_path --update >> $LOG_FILE 2>&1" # --- Improved Cron Job Handling --- print_info "Checking/Updating cron job for Pulse automatic updates..." # Get current crontab content or empty string if none exists current_cron=$(crontab -l -u root 2>/dev/null || true) # --- Use awk to remove existing blocks --- # Escape the identifier for awk pattern matching (ensure literal match) local escaped_identifier_for_awk # Escape for extended regex used by awk escaped_identifier_for_awk=$(echo "$cron_identifier" | sed 's/[][\\.*^$(){}|+?]/\\\\&/g') local awk_script='/__PATTERN__/ { skip=1; next } skip == 1 { skip=0; next } { print }' # Replace placeholder with escaped identifier, using ^ to anchor at start of line awk_script="${awk_script/__PATTERN__/^$escaped_identifier_for_awk$}" # Filter the crontab, preserving other lines filtered_cron=$(echo "$current_cron" | awk "$awk_script") # --- End awk filtering --- # Prepare the new crontab content local new_cron_content # Trim potential leading/trailing whitespace/newlines from filtered_cron trimmed_filtered_cron=$(echo "$filtered_cron" | awk 'NF') # Get non-empty lines if [ -n "$trimmed_filtered_cron" ]; then # Append the new block with a preceding newline new_cron_content=$(printf "%s\n%s\n%s" "$trimmed_filtered_cron" "$cron_identifier" "$cron_command") else # Filtered cron was empty, just add the new block new_cron_content=$(printf "%s\n%s" "$cron_identifier" "$cron_command") fi # Load the new crontab content 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" # Ensure log file exists and is writable 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 # --- End Improved Cron Job Handling --- return 0 } prompt_for_cron_setup() { # Only prompt if not running in update mode if [ -n "$MODE_UPDATE" ]; then # Check if MODE_UPDATE is non-empty return 0 fi local cron_exists=false # Define the exact pattern to search for, escaping regex characters for grep local cron_identifier="# Pulse-Auto-Update [$SCRIPT_NAME]" # Use the variable local identifier_for_grep_check # Escape only basic regex metachars for standard grep -q identifier_for_grep_check=$(echo "$cron_identifier" | sed 's/[]$.*^[]/\\&/g') # Check if cron job exists for root user, anchored to start of line # Use grep -q for silent check based on exit status if crontab -l -u root 2>/dev/null | grep -q "^${identifier_for_grep_check}$"; then cron_exists=true fi echo "" # Add spacing 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]" # Clarification 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 ;; # Call function to remove the job 3) setup_cron_update ;; # Call function to prompt for new schedule and 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]" # Clarification read -p "Do you want to set up automatic updates for Pulse? [Y/n]: " setup_cron_confirm if [[ ! "$setup_cron_confirm" =~ ^[Nn]$ ]]; then # Proceed if not 'N' or 'n' [Default Yes] setup_cron_update else print_info "Skipping automatic update setup." fi fi } final_instructions() { # Try to get the IP address of the container local ip_address ip_address=$(hostname -I | awk '{print $1}') # <--- Line 1155 (UNCOMMENTED) local port_value # Read the port from the .env file, fallback to default if not found/readable 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" # Default port fi # Get current checked out tag for final message local final_tag="" if [ -d "$PULSE_DIR/.git" ]; then cd "$PULSE_DIR" || print_warning "Could not cd to $PULSE_DIR to get final tag." final_tag=$(get_current_local_tag) cd .. # Go back fi echo "" print_success "Pulse for Proxmox VE installation/update complete!" if [ -n "$final_tag" ]; then print_success "Current version installed: $final_tag" fi echo "-------------------------------------------------------------" print_info "You should be able to access the Pulse dashboard at:" if [ -n "$ip_address" ]; then # <--- UNCOMMENTED echo " http://$ip_address:$port_value" # <--- UNCOMMENTED else # <--- UNCOMMENTED echo " http://:$port_value" print_warning "Could not automatically determine the LXC IP address." fi # <--- UNCOMMENTED 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 "-------------------------------------------------------------" } # --- Main Execution --- Refactored check_root # ---> MODIFIED: Check flag instead of env var < --- if [ -z "$MODE_UPDATE" ] && [ "$INSTALLER_WAS_REEXECUTED" != "true" ]; then print_dependency_info fi # Check for self-update - runs regardless of whether info was printed self_update_check || print_warning "Installer self-check failed, proceeding anyway..." # Check installation status and determine user's desired action first # This also determines TARGET_TAG check_installation_status_and_determine_action # --- Execute Action Based on INSTALL_MODE --- case "$INSTALL_MODE" in "remove") print_info "Proceeding with removal..." perform_remove exit $? ;; "cancel") print_info "Operation cancelled by user." exit 0 ;; "error") # Error message already printed print_error "Exiting due to error." exit 1 ;; "install" | "update") # ---> Self-update check moved to the beginning <--- # Only install dependencies if installing or updating print_info "Proceeding with install/update. Installing prerequisites..." apt_update_upgrade || exit 1 install_dependencies || exit 1 # Installs git, curl, diffutils setup_node || exit 1 create_pulse_user || exit 1 print_success "Prerequisites installed." # Now perform the specific action if [ "$INSTALL_MODE" = "install" ]; then print_info "Starting installation..." # --- Installation specific steps --- # If TARGET_TAG was specified, check it exists BEFORE cloning if [ -n "$SPECIFIED_VERSION_TAG" ]; then # Check tag existence remotely using ls-remote [requires git] 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 print_info "Cloning Pulse repository into $PULSE_DIR..." # Clone shallowly initially? No, need history for tags. 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; } # Set ownership early so subsequent git commands run as pulse user chown -R "$PULSE_USER":"$PULSE_USER" "$PULSE_DIR" || print_warning "Failed initial chown after clone." else print_error "Failed to clone repository." exit 1 fi # Determine TARGET_TAG if not specified [find latest tag] if [ -z "$TARGET_TAG" ]; then TARGET_TAG=$(get_latest_remote_tag) # Already fetched during check if update, fetch here if install if [ $? -ne 0 ] || [ -z "$TARGET_TAG" ]; then print_error "Could not determine latest release tag to install." cd ..; exit 1 fi print_info "Determined latest version tag: $TARGET_TAG" fi # Checkout the target tag 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." cd .. # Back to original dir before dependency install install_npm_deps || exit 1 # Installs root and server [NOW INCLUDES DEV] # Build CSS after dependencies print_info "Building CSS assets..." cd "$PULSE_DIR" || { print_error "Failed to cd to $PULSE_DIR before building CSS"; exit 1; } if ! npm run build:css > /dev/null 2>&1; then print_error "Failed to build CSS assets." exit 1 else print_success "CSS assets built." fi # Now cd back if needed, though subsequent steps might need PULSE_DIR cd .. set_permissions || exit 1 # Set permissions AFTER building css configure_environment || exit 1 # Prompt user for details setup_systemd_service || exit 1 # Create, enable, start service final_instructions prompt_for_cron_setup # Ask about cron on fresh install else # Update mode [TARGET_TAG determined during check] print_info "Starting update to version $TARGET_TAG..." # --- Update specific steps --- if perform_update; then # perform_update now uses TARGET_TAG 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."