diff --git a/scripts/install-pulse.sh b/scripts/install-pulse.sh old mode 100644 new mode 100755 index 1df6f2710..e765999a5 --- a/scripts/install-pulse.sh +++ b/scripts/install-pulse.sh @@ -1,10 +1,12 @@ #!/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) +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" @@ -13,12 +15,14 @@ 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="a29105e" # Replaced placeholder with actual commit SHA # --- Flags & Variables --- -MODE_UPDATE=false # Flag to run in non-interactive update mode +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 @@ -41,13 +45,17 @@ fi # Consume arguments until --version is found, then expect its value while [[ "$#" -gt 0 ]]; do case $1 in - --update) MODE_UPDATE=true; shift ;; + --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 + echo "Error: --version requires a tag name [e.g., v3.2.3]" >&2 exit 1 fi ;; @@ -69,7 +77,7 @@ print_warning() { } print_error() { - echo -e "\033[1;31m[ERROR]\033[0m $1" >&2 + echo -e "\033[1;31m[ERROR]\033[0m $1 >&2" } check_root() { @@ -79,78 +87,213 @@ check_root() { fi } -# --- Self-Update Function --- +# ---> NEW: Function to inform about dependencies < --- +print_dependency_info() { + # --- ASCII Banner --- + echo -e "\033[1;34m" + echo " ____ _ " + echo "| _ \ _ _| |___ ___ " + echo "| |_) | | | | / __|/ _ \" + echo "| __/| |_| | \__ \ __/" + echo "|_| \__,_|_|___/\___|" + echo " " + echo "\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 "" + # --- End ASCII Banner --- + + print_info "-----------------------------------------------------" + print_info "This script requires the following dependencies:" + print_info " - Standard tools: curl, git, sudo, gpg, diffutils" + print_info " (These will be installed if missing via apt-get)" + print_info " - Node.js & npm: For running Pulse" + print_info " (Will be installed via NodeSource repository setup)" + print_info " - jq: For checking for installer script updates" + print_info " (If missing, the script will *attempt* to install it" + print_info " using 'apt-get install jq' to enable self-updates.)" + print_info "-----------------------------------------------------" + # --- 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 ] || [ "$MODE_UPDATE" = true ] || [ -z "$SCRIPT_ABS_PATH" ]; then + if [ ! -t 0 ] || [ -n "$MODE_UPDATE" ] || [ -z "$SCRIPT_ABS_PATH" ]; then # Check if MODE_UPDATE is non-empty return 0 fi - # Check if curl and diff are available (should be after dependency install) - if ! command -v curl &> /dev/null || ! command -v diff &> /dev/null; then - print_warning "curl or diff not found. Cannot check for installer updates." + # --- Dependencies Check --- + if ! command -v curl &> /dev/null; then + print_warning "curl command not found, skipping installer self-update check." return 0 fi - - print_info "Checking for updates to the installer script itself..." - local temp_script="/tmp/${SCRIPT_NAME}.tmp" - - if curl -sL "$SCRIPT_RAW_URL" -o "$temp_script"; then - # Compare the downloaded script with the running script - if ! diff -q "$SCRIPT_ABS_PATH" "$temp_script" >/dev/null 2>&1; then - print_warning "A newer version of the installation script is available." - read -p "Do you want to update the installer and re-run? (Y/n): " update_confirm - if [[ ! "$update_confirm" =~ ^[Nn]$ ]]; then # Default Yes - print_info "Updating installer script..." - # Make the new script executable - if ! chmod +x "$temp_script"; then - print_error "Failed to make temporary script executable." - rm -f "$temp_script" - return 1 # Continue with old script? - fi - # Replace the current script - if ! mv "$temp_script" "$SCRIPT_ABS_PATH"; then - print_error "Failed to replace the current script file." - rm -f "$temp_script" # Clean up if mv failed - return 1 - fi - print_success "Installer updated successfully." - print_info "Please re-run the script using: sudo bash $SCRIPT_ABS_PATH" - exit 0 # Exit cleanly, user needs to re-run + 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 - print_info "Skipping installer update. Continuing with the current version." - rm -f "$temp_script" - return 0 + 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_info "Installer script is up-to-date." - rm -f "$temp_script" + 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..." + # Use sed to find the line starting with CURRENT_SCRIPT_COMMIT_SHA= and replace the quoted value + # Using # as delimiter for sed to avoid issues with slashes in SHAs (unlikely, but safe) + sed -i "s#^CURRENT_SCRIPT_COMMIT_SHA=.*#CURRENT_SCRIPT_COMMIT_SHA=\"$latest_remote_sha\"#" "$temp_script" + local update_sha_exit_code=$? + if [ $update_sha_exit_code -ne 0 ]; then + print_error "sed command failed while updating embedded SHA! Aborting update." + print_error "Exit code: $update_sha_exit_code" + rm -f "$temp_script" + return 1 + fi + # --- END NEW STEP --- + + # ---> Fix line endings on TEMP file < --- + # Keep this step to ensure downloaded script content is clean + sed -i 's/\r$//' "$temp_script" + local sed_exit_code=$? + if [ $sed_exit_code -ne 0 ]; then + print_error "sed command failed! Cannot fix line endings. Aborting." + rm -f "$temp_script" + return 1 # Return instead of exit + fi + # ---> Remove hash calculation of temp file --- + + if ! chmod +x "$temp_script"; then + print_error "Failed to make temporary script executable." + rm -f "$temp_script" + return 1 + fi + # --- Move the FIXED temp file into place --- + if ! mv "$temp_script" "$SCRIPT_ABS_PATH"; then + print_error "Failed to replace the current script file." + rm -f "$temp_script" # Clean up if mv failed + return 1 + fi + 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_warning "Could not download the latest installer script for comparison." - rm -f "$temp_script" # Clean up potential partial file - return 0 # Continue with current script + 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) +# 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..." - # Use sudo only if not already root (though check_root ensures we are) + 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." + 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 + # 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." + print_warning "Could not determine the latest remote release tag." >&2 # Also redirect warning return 1 fi - echo "$latest_tag" + echo "$latest_tag" # This is the intended stdout return 0 } @@ -191,10 +334,10 @@ get_current_local_tag() { # --- Installation Status Check & Action Determination --- check_installation_status_and_determine_action() { # Check if running in non-interactive update mode FIRST - if [ "$MODE_UPDATE" = true ]; then + 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) + # 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 @@ -240,16 +383,16 @@ check_installation_status_and_determine_action() { 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) + 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" + 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)." + 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." @@ -276,55 +419,70 @@ check_installation_status_and_determine_action() { # 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 "Choose an action:" - echo " 1) Install specified version ($SPECIFIED_VERSION_TAG)" - echo " 2) Remove Pulse" - echo " 3) Cancel" - read -p "Enter your choice (1-3): " user_choice + printf '%s\n' "Choose an action:" + printf ' %s) %s %s\n' "1" "Install specified version" "$SPECIFIED_VERSION_TAG" + printf ' %s) %s\n' "2" "Remove Pulse" + printf ' %s) %s\n' "3" "Cancel" + printf ' %s) %s\n' "4" "Manage automatic updates" + read -p "Enter your choice [1-4]: " user_choice # Updated range case $user_choice in 1) INSTALL_MODE="update" ;; # Treat re-run as 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 # Up to date and no specific version requested OR specified matches current - echo "Choose an action:" - echo " 1) Re-install current version ($current_tag)" # Changed prompt - echo " 2) Remove Pulse" - echo " 3) Cancel" - read -p "Enter your choice (1-3): " user_choice + printf '%s\n' "Choose an action:" + printf ' %s) %s %s\n' "1" "Re-install current version" "$current_tag" + printf ' %s) %s\n' "2" "Remove Pulse" + printf ' %s) %s\n' "3" "Cancel" + printf ' %s) %s\n' "4" "Manage automatic updates" + read -p "Enter your choice [1-4]: " user_choice # Updated range case $user_choice in 1) INSTALL_MODE="update" ;; # Treat re-run as 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 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" + printf '%s\n' "Choose an action:" + printf ' %s) %s %s\n' "1" "Install specified version" "$SPECIFIED_VERSION_TAG" + printf ' %s) %s\n' "2" "Remove Pulse" + printf ' %s) %s\n' "3" "Cancel" + printf ' %s) %s\n' "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" + printf '%s\n' "Choose an action:" + printf ' %s) %s %s\n' "1" "Update Pulse to the latest version" "$TARGET_TAG" + printf ' %s) %s\n' "2" "Remove Pulse" + printf ' %s) %s\n' "3" "Cancel" + printf ' %s) %s\n' "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 - read -p "Enter your choice (1-3): " user_choice - case $user_choice in - 1) INSTALL_MODE="update" ;; - 2) INSTALL_MODE="remove" ;; - 3) INSTALL_MODE="cancel" ;; - *) print_error "Invalid choice."; INSTALL_MODE="error" ;; - esac 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." + 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 @@ -343,9 +501,9 @@ check_installation_status_and_determine_action() { # 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 " 1) Install Pulse [latest version]" echo " 2) Cancel" - read -p "Enter your choice (1-2): " user_choice + 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" ;; @@ -356,7 +514,7 @@ check_installation_status_and_determine_action() { # 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) +# --- 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 @@ -368,8 +526,9 @@ apt_update_upgrade() { } install_dependencies() { - print_info "Installing necessary dependencies (git, curl, sudo, gpg)..." - if apt-get install -y git curl sudo gpg > /dev/null; then + 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." @@ -378,16 +537,16 @@ install_dependencies() { } setup_node() { - print_info "Setting up Node.js repository (NodeSource)..." + 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." + 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..." + 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..." @@ -395,7 +554,7 @@ setup_node() { # 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)." + print_error "curl is required but not found. Please install it first [apt-get install curl]." exit 1 fi @@ -446,7 +605,7 @@ create_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) + # 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." @@ -460,7 +619,7 @@ create_pulse_user() { } -# --- Core Action Functions --- (perform_update, perform_remove) +# --- 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 @@ -470,13 +629,43 @@ perform_update() { 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." - print_info "Fetching latest changes and tags from git (running as user $PULSE_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 @@ -485,6 +674,7 @@ perform_update() { 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 @@ -493,6 +683,7 @@ perform_update() { # 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 @@ -501,27 +692,41 @@ perform_update() { 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)..." + 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 + # Continue anyway, as the core update [checkout] succeeded fi - # Ensure the script itself remains executable after update (No change here) + # ---> 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..." + 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." + print_warning "Could not find script path $SCRIPT_ABS_PATH to ensure executable permission." fi - print_info "Re-installing npm dependencies (root)..." + 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 @@ -550,20 +755,20 @@ perform_update() { 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_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)..." + 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)." + 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_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 @@ -573,19 +778,19 @@ perform_update() { 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)" + 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)." + 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 + 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 @@ -601,14 +806,14 @@ perform_remove() { 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)..." + 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)..." + 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)..." + 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." @@ -623,7 +828,7 @@ perform_remove() { 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)..." + 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." @@ -641,7 +846,7 @@ perform_remove() { systemctl daemon-reload fi - print_info "Removing Pulse installation directory ($PULSE_DIR)..." + print_info "Removing Pulse installation directory $PULSE_DIR..." if rm -rf "$PULSE_DIR"; then print_success "Installation directory removed." else @@ -653,7 +858,7 @@ perform_remove() { return 0 } -# --- Installation Step Functions --- (install_npm_deps, set_permissions, configure_environment, setup_systemd_service) +# --- 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 @@ -664,9 +869,9 @@ install_npm_deps() { cd "$PULSE_DIR" || { print_error "Failed to change directory to $PULSE_DIR"; return 1; } - print_info "Installing root dependencies (including dev)..." + print_info "Installing root dependencies [including dev]..." # Use --unsafe-perm if running npm install as root, which might be necessary for some packages - # REMOVED --omit=dev to ensure build tools like postcss/autoprefixer are present + # 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 @@ -674,9 +879,8 @@ install_npm_deps() { return 1 fi - print_info "Installing server dependencies (including dev)..." + print_info "Installing server dependencies [including dev]..." cd server || { print_error "Failed to change directory to $PULSE_DIR/server"; cd ..; return 1; } - # REMOVED --omit=dev if npm install --unsafe-perm > /dev/null 2>&1; then print_success "Server dependencies installed." else @@ -718,7 +922,7 @@ configure_environment() { 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 + 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 @@ -730,7 +934,7 @@ configure_environment() { fi fi - # --- Gather Proxmox Details (Only if interactive) --- + # --- Gather Proxmox Details [Only if interactive] --- local proxmox_host="" local proxmox_token_id="" local proxmox_token_secret="" @@ -739,10 +943,10 @@ configure_environment() { 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 + 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 + read -p " -> Proxmox Host URL [e.g., https://192.168.1.100:8006]: " proxmox_host done # Validate and potentially prepend https:// @@ -752,16 +956,15 @@ configure_environment() { print_info "Using Proxmox Host URL: $proxmox_host" fi - # --- Display Token Generation Info --- (Remains the same) - # ... (token info display code) ... + # --- 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 ' # 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 ' # 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:' @@ -771,10 +974,10 @@ configure_environment() { echo "and paste them below." echo "" - read -p " -> Proxmox API Token ID (e.g., user@pam!tokenid): " proxmox_token_id + 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 + read -p " -> Proxmox API Token ID [e.g., user@pam!tokenid]: " proxmox_token_id done read -sp " -> Proxmox API Token Secret: " proxmox_token_secret @@ -785,19 +988,19 @@ configure_environment() { echo done - # --- Optional Settings --- (Remains the same) - 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 + # --- 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) + # 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) + # Determine values [use defaults if not set interactively] local self_signed_value="true" if [[ "$allow_self_signed" =~ ^[Nn]$ ]]; then self_signed_value="false"; fi @@ -810,10 +1013,10 @@ configure_environment() { fi fi - # --- Create .env file --- (Handle case where variables might be empty if non-interactive) + # --- 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) + # 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" @@ -821,7 +1024,7 @@ configure_environment() { 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 "Skipping variable substitution in .env as no values were provided [non-interactive?]." print_warning "Please edit $env_path manually." fi @@ -841,17 +1044,17 @@ 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)..." + print_info "Setting up systemd service $SERVICE_NAME..." local service_file="/etc/systemd/system/$SERVICE_NAME" - # Find Node path (needed for systemd ExecStart) + # 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) + # Find npm path [needed for systemd ExecStart] local npm_path npm_path=$(command -v npm) if [ -z "$npm_path" ]; then @@ -892,7 +1095,7 @@ ExecStart=$node_path $npm_path run start Restart=on-failure RestartSec=5 -# Environment (optional, if needed, but .env should handle this) +# Environment [optional, if needed, but .env should handle this] # Environment="NODE_ENV=production" # Standard output/error logging @@ -916,7 +1119,7 @@ EOF # Skip enable/start if in update mode if [ "$update_mode" = true ]; then - print_info "(Update mode: Skipping service enable/start)" + print_info "[Update mode: Skipping service enable/start]" return 0 fi @@ -944,34 +1147,46 @@ EOF } -# --- 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) +# --- 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)" - local escaped_cron_identifier - escaped_cron_identifier=$(sed 's/[/.*^$]/\\&/g' <<< "$cron_identifier") + 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 - if ! echo "$current_cron" | grep -q "^${escaped_cron_identifier}$"; then + # 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 sed to remove the identifier line and the line immediately following it. - filtered_cron=$(echo "$current_cron" | sed "/^${escaped_cron_identifier}$/{N;d;}") + # --- 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 - # Handle case where removing the job leaves the crontab empty - if [ -z "$filtered_cron" ]; then - echo "" | crontab -u root - + # 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 "$filtered_cron" | crontab -u root - + echo "$trimmed_filtered_cron" | crontab -u root - # Load filtered content fi if [ $? -eq 0 ]; then @@ -987,23 +1202,19 @@ setup_cron_update() { local cron_schedule="" local cron_command="" local script_path="$SCRIPT_ABS_PATH" - local escaped_script_path # For grep/sed patterns - local cron_identifier="# Pulse-Auto-Update ($SCRIPT_NAME)" # Identifier comment - local escaped_cron_identifier + 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 - # Escape necessary characters for sed pattern matching - escaped_cron_identifier=$(sed 's/[/.*^$]/\\&/g' <<< "$cron_identifier") - print_info "Choose update frequency (will update to the *latest release tag*):" # Clarified update target + 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 + echo " 4) Never [Cancel]" + read -p "Enter your choice [1-4]: " freq_choice case $freq_choice in 1) cron_schedule="@daily" ;; @@ -1013,7 +1224,7 @@ setup_cron_update() { *) print_error "Invalid choice. Skipping auto-update setup."; return 1 ;; esac - # Construct the cron command (keeps using --update flag, logic inside update handles finding latest tag) + # Construct the cron command cron_command="$cron_schedule /usr/bin/bash $script_path --update >> $LOG_FILE 2>&1" # --- Improved Cron Job Handling --- @@ -1021,17 +1232,30 @@ setup_cron_update() { # Get current crontab content or empty string if none exists current_cron=$(crontab -l -u root 2>/dev/null || true) - # Use sed to remove the identifier line and the line immediately following it. - # The pattern looks for the exact identifier comment at the beginning of a line (^). - # If found, it reads the Next line (N) and Deletes both (d). - filtered_cron=$(echo "$current_cron" | sed "/^${escaped_cron_identifier}$/{N;d;}") + # --- 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 - # If filtered_cron is empty after removal, avoid leading newline. Otherwise, add newline before appending. - if [ -z "$filtered_cron" ]; then - new_cron_content=$(printf "%s\\n%s" "$cron_identifier" "$cron_command") + 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 - new_cron_content=$(printf "%s\\n%s\\n%s" "$filtered_cron" "$cron_identifier" "$cron_command") + # 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 @@ -1041,7 +1265,7 @@ setup_cron_update() { 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 to root" + 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." @@ -1054,14 +1278,20 @@ setup_cron_update() { prompt_for_cron_setup() { # Only prompt if not running in update mode - if [ "$MODE_UPDATE" = true ]; then + if [ -n "$MODE_UPDATE" ]; then # Check if MODE_UPDATE is non-empty return 0 fi - local cron_identifier="# Pulse-Auto-Update ($SCRIPT_NAME)" local cron_exists=false - # Check if cron job exists for root user - if crontab -l -u root 2>/dev/null | grep -q "$cron_identifier"; then + # 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 @@ -1069,24 +1299,24 @@ prompt_for_cron_setup() { 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 + print_info "[Cron job will update to the latest release tag when run]" # Clarification echo "Choose an action:" - echo " 1) Change update schedule" + echo " 1) Keep current schedule [Do nothing]" echo " 2) Disable automatic updates" - echo " 3) Keep current schedule (Do nothing)" - read -p "Enter your choice (1-3): " cron_manage_choice + echo " 3) Change update schedule" + read -p "Enter your choice [1-3]: " cron_manage_choice case $cron_manage_choice in - 1) setup_cron_update ;; # Call function to prompt for new schedule and update + 1) print_info "Keeping current automatic update schedule.";; 2) disable_cron_update ;; # Call function to remove the job - 3) print_info "Keeping current automatic update schedule.";; + 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) + 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." @@ -1129,10 +1359,9 @@ final_instructions() { 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 "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" - print_info "Configuration file: $PULSE_DIR/.env" echo "-------------------------------------------------------------" } @@ -1140,6 +1369,14 @@ final_instructions() { # --- 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 @@ -1161,17 +1398,16 @@ case "$INSTALL_MODE" in 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, diff + install_dependencies || exit 1 # Installs git, curl, diffutils setup_node || exit 1 create_pulse_user || exit 1 print_success "Prerequisites installed." - # ---> ADDED: Check for self-update AFTER dependencies are installed <--- - self_update_check || print_warning "Installer self-check failed, continuing..." - # Now perform the specific action if [ "$INSTALL_MODE" = "install" ]; then print_info "Starting installation..." @@ -1179,8 +1415,7 @@ case "$INSTALL_MODE" in # 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) - # Use check_remote_tag_exists but don't cd into repo + # 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 @@ -1202,7 +1437,7 @@ case "$INSTALL_MODE" in exit 1 fi - # Determine TARGET_TAG if not specified (find latest tag) + # 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 @@ -1221,7 +1456,7 @@ case "$INSTALL_MODE" in 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) + install_npm_deps || exit 1 # Installs root and server [NOW INCLUDES DEV] # Build CSS after dependencies print_info "Building CSS assets..." @@ -1241,7 +1476,7 @@ case "$INSTALL_MODE" in final_instructions prompt_for_cron_setup # Ask about cron on fresh install - else # Update mode (TARGET_TAG determined during check) + 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