Files
ProxMenux/install_proxmenux.sh
T
MacRimi fc41b04e87 Pre-release 1.2.7: OCI fixes, 12 new catalog applications and docs updates
Last changes before version 1.2.7.

OCI manager Apps
- Multi-container applications start while one of their containers is
  being backed up: the start hook waits until vzdump releases it and does
  not start it twice. The DHCP client of a dependency no longer keeps the
  lock of the hook, so later starts do not hang, and the watchdog waits
  while a dependency is locked. Installing or updating ProxMenux replaces
  the published copies of the hook already in place.
- Images built from scratch with a numeric user, such as ddns-updater,
  install: the container is created from a copy of the image that adds
  the /etc/passwd and /etc/group Proxmox VE needs, and the application
  runs with the user the image declares. The downloaded image stays
  unchanged. An image with a named user and no user database stops
  before anything is created, with a message that says why.

Catalog
- Twelve new applications: MDBListarr, CrossWatch, Free Games Claimer
  Remaster, Cloudreve, Poznote and AceStream HTTP Proxy, suggested in
  discussion #440, and the Dockur images of Chrony, Dnsmasq, Tor,
  Stunnel, strfry and Statping. The catalog lists 347 applications.
- Their prompts, labels and notes are translated into the seven
  languages of the menus.

Documentation
- OCI manager: architecture, lifecycle and stacks pages revised, with
  larger diagrams and new screenshots.
- Contributors: the page now thanks those developing, reviewing,
  translating and testing ProxMenux today and those who helped in its
  first steps, with new avatars.

Tests for the stack start hook, the watchdog, the user database check
of OCI images and the Tor installation summary.
2026-10-25 13:17:54 +01:00

1015 lines
36 KiB
Bash
Executable File

#!/bin/bash
# ==========================================================
# ProxMenux - A menu-driven toolkit for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Contributors : cod378
# Subproject : ProxMenux Monitor (System Health & Web Dashboard)
# Copyright : (c) 2024-2025 MacRimi
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.4
# Last Updated : 12/11/2025
# ==========================================================
# Description:
# This script installs and configures ProxMenux, a menu-driven
# toolkit for managing and optimizing Proxmox VE servers.
#
# - Ensures the script is run with root privileges.
# - Displays an installation confirmation prompt.
# - Installs required dependencies:
# • whiptail (interactive terminal menus)
# • curl (downloads and connectivity checks)
# • jq (JSON parsing)
# - Installs pre-generated JSON language catalogs (no translation runtime).
# - Creates the ProxMenux base directories and configuration files:
# • $BASE_DIR/config.json
# • $BASE_DIR/cache.json
# - Copies local project files into the target paths (offline mode by default):
# • scripts/* → $BASE_DIR/scripts/
# • utils.sh → $BASE_DIR/scripts/utils.sh
# • menu → $INSTALL_DIR/menu (main launcher)
# • install_proxmenux.sh → $BASE_DIR/install_proxmenux.sh
# - Sets correct permissions for all executables.
# - Displays the final instruction on how to start ProxMenux ("menu").
#
# Notes:
# - This installer supports both offline and online setups.
# - ProxMenux Monitor can be installed later as an optional module
# to provide real-time system monitoring and a web dashboard.
# ==========================================================
# Configuration ============================================
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
INSTALL_DIR="/usr/local/bin"
BASE_DIR="/usr/local/share/proxmenux"
CONFIG_FILE="$BASE_DIR/config.json"
UTILS_FILE="$BASE_DIR/utils.sh"
LOCAL_VERSION_FILE="$BASE_DIR/version.txt"
MENU_SCRIPT="menu"
# Legacy path that existed during the Python+googletrans era. The current
# translate flow uses pre-generated JSON files in lang/ — no virtualenv,
# no online translation at runtime — so this path is purged on install
# if present. Kept as a literal here so the cleanup is grep-able.
LEGACY_VENV_PATH="/opt/googletrans-env"
MONITOR_INSTALL_DIR="$BASE_DIR"
MONITOR_RUNTIME_DIR="$BASE_DIR/monitor-app"
MONITOR_SERVICE_FILE="/etc/systemd/system/proxmenux-monitor.service"
MONITOR_PORT=8008
# Offline installer envs
REPO_URL="https://github.com/MacRimi/ProxMenux.git"
TEMP_DIR="/tmp/proxmenux-install-$$"
# Load utility functions
NEON_PURPLE_BLUE="\033[38;5;99m"
WHITE="\033[38;5;15m"
RESET="\033[0m"
DARK_GRAY="\033[38;5;244m"
ORANGE="\033[38;5;208m"
YW="\033[33m"
YWB="\033[1;33m"
MG="\033[1;35m"
GN="\033[1;92m"
RD="\033[01;31m"
CL="\033[m"
BL="\033[36m"
DGN="\e[32m"
BGN="\e[1;32m"
DEF="\e[1;36m"
CUS="\e[38;5;214m"
BOLD="\033[1m"
BFR="\\r\\033[K"
HOLD="-"
BOR=" | "
CM="${GN}✓ ${CL}"
TAB=" "
# Create and display spinner
spinner() {
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local spin_i=0
local interval=0.1
printf "\e[?25l"
local color="${MG}"
while true; do
printf "\r ${color}%s${CL}" "${frames[spin_i]}"
spin_i=$(( (spin_i + 1) % ${#frames[@]} ))
sleep "$interval"
done
}
# Function to simulate typing effect
type_text() {
local text="$1"
local delay=0.05
for ((i=0; i<${#text}; i++)); do
echo -n "${text:$i:1}"
sleep $delay
done
echo
}
# Display info message with spinner
msg_info() {
local msg="$1"
echo -ne "${TAB}${MG}${HOLD}${msg}"
spinner &
SPINNER_PID=$!
}
# Display info2 message
msg_info2() {
local msg="$1"
echo -e "${TAB}${BOLD}${YW}${HOLD}${msg}${CL}"
}
# Display title script
msg_title() {
local msg="$1"
echo -e "\n"
echo -e "${TAB}${BOLD}${HOLD}${BOR}${msg}${BOR}${HOLD}${CL}"
echo -e "\n"
}
# Display warning or highlighted information message
msg_warn() {
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then
kill $SPINNER_PID > /dev/null
fi
printf "\e[?25h"
local msg="$1"
echo -e "${BFR}${TAB}${CL} ${YWB}${msg}${CL}"
}
# Display success message
msg_ok() {
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then
kill $SPINNER_PID > /dev/null
fi
printf "\e[?25h"
local msg="$1"
echo -e "${BFR}${TAB}${CM}${GN}${msg}${CL}"
}
# Display error message
msg_error() {
if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then
kill $SPINNER_PID > /dev/null
fi
printf "\e[?25h"
local msg="$1"
echo -e "${BFR}${TAB}${RD}[ERROR] ${msg}${CL}"
}
show_proxmenux_logo() {
clear
if [[ -z "$SSH_TTY" && -z "$(who am i | awk '{print $NF}' | grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}')" ]]; then
# Logo for terminal noVNC
LOGO=$(cat << "EOF"
\e[0m\e[38;2;61;61;61m▆\e[38;2;60;60;60m▄\e[38;2;54;54;54m▂\e[0m \e[38;2;0;0;0m \e[0m \e[38;2;54;54;54m▂\e[38;2;60;60;60m▄\e[38;2;61;61;61m▆\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[38;2;61;61;61;48;2;37;37;37m▇\e[0m\e[38;2;60;60;60m▅\e[38;2;56;56;56m▃\e[38;2;37;37;37m▁ \e[38;2;36;36;36m▁\e[38;2;56;56;56m▃\e[38;2;60;60;60m▅\e[38;2;61;61;61;48;2;37;37;37m▇\e[48;2;62;62;62m \e[0m\e[7m\e[38;2;60;60;60m▁\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[7m\e[38;2;61;61;61m▂\e[0m\e[38;2;62;62;62;48;2;61;61;61m┈\e[48;2;62;62;62m \e[48;2;61;61;61m┈\e[0m\e[38;2;60;60;60m▆\e[38;2;57;57;57m▄\e[38;2;48;48;48m▂\e[0m \e[38;2;47;47;47m▂\e[38;2;57;57;57m▄\e[38;2;60;60;60m▆\e[38;2;62;62;62;48;2;61;61;61m┈\e[48;2;62;62;62m \e[48;2;61;61;61m┈\e[0m\e[7m\e[38;2;60;60;60m▂\e[38;2;57;57;57m▄\e[38;2;47;47;47m▆\e[0m \e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[7m\e[38;2;39;39;39m▇\e[38;2;57;57;57m▅\e[38;2;60;60;60m▃\e[0m\e[38;2;40;40;40;48;2;61;61;61m▁\e[48;2;62;62;62m \e[38;2;54;54;54;48;2;61;61;61m┊\e[48;2;62;62;62m \e[38;2;39;39;39;48;2;61;61;61m▁\e[0m\e[7m\e[38;2;60;60;60m▃\e[38;2;57;57;57m▅\e[38;2;38;38;38m▇\e[0m \e[38;2;193;60;2m▃\e[38;2;217;67;2m▅\e[38;2;225;70;2m▇\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[0m \e[38;2;203;63;2m▄\e[38;2;147;45;1m▂\e[0m \e[7m\e[38;2;55;55;55m▆\e[38;2;60;60;60m▄\e[38;2;61;61;61m▂\e[38;2;60;60;60m▄\e[38;2;55;55;55m▆\e[0m \e[38;2;144;44;1m▂\e[38;2;202;62;2m▄\e[38;2;219;68;2m▆\e[38;2;231;72;3;48;2;226;70;2m┈\e[48;2;231;72;3m \e[48;2;225;70;2m▉\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[7m\e[38;2;121;37;1m▉\e[0m\e[38;2;0;0;0;48;2;231;72;3m \e[0m\e[38;2;221;68;2m▇\e[38;2;208;64;2m▅\e[38;2;212;66;2m▂\e[38;2;123;37;0m▁\e[38;2;211;65;2m▂\e[38;2;207;64;2m▅\e[38;2;220;68;2m▇\e[48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m┈\e[0m\e[7m\e[38;2;221;68;2m▂\e[0m\e[38;2;44;13;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏\e[0m \e[7m\e[38;2;190;59;2m▅\e[38;2;216;67;2m▃\e[38;2;225;70;2m▁\e[0m\e[38;2;95;29;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;230;71;2m┈\e[48;2;231;72;3m \e[0m\e[7m\e[38;2;225;70;2m▁\e[38;2;216;67;2m▃\e[38;2;191;59;2m▅\e[0m \e[38;2;0;0;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏ \e[0m \e[7m\e[38;2;172;53;1m▆\e[38;2;213;66;2m▄\e[38;2;219;68;2m▂\e[38;2;213;66;2m▄\e[38;2;174;54;2m▆\e[0m \e[38;2;0;0;0m \e[0m \e[38;2;0;0;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
\e[38;2;59;59;59;48;2;62;62;62m▏ \e[0m\e[38;2;32;32;32m▏ \e[0m \e[38;2;0;0;0;48;2;231;72;3m \e[38;2;231;72;3;48;2;225;70;2m▉\e[0m
\e[7m\e[38;2;52;52;52m▆\e[38;2;59;59;59m▄\e[38;2;61;61;61m▂\e[0m\e[38;2;31;31;31m▏ \e[0m \e[7m\e[38;2;228;71;2m▂\e[38;2;221;69;2m▄\e[38;2;196;60;2m▆\e[0m
EOF
)
TEXT=(
""
""
"${BOLD}ProxMenux${RESET}"
""
"${BOLD}${NEON_PURPLE_BLUE}An Interactive Menu for${RESET}"
"${BOLD}${NEON_PURPLE_BLUE}Proxmox VE management${RESET}"
""
""
""
""
)
mapfile -t logo_lines <<< "$LOGO"
for i in {0..9}; do
echo -e "${TAB}${logo_lines[i]} ${WHITE}│${RESET} ${TEXT[i]}"
done
echo -e
else
# Logo for terminal SSH
TEXT=(
""
""
""
""
"${BOLD}ProxMenux${RESET}"
""
"${BOLD}${NEON_PURPLE_BLUE}An Interactive Menu for${RESET}"
"${BOLD}${NEON_PURPLE_BLUE}Proxmox VE management${RESET}"
""
""
""
""
""
""
)
LOGO=(
"${DARK_GRAY}░░░░ ░░░░${RESET}"
"${DARK_GRAY}░░░░░░░ ░░░░░░ ${RESET}"
"${DARK_GRAY}░░░░░░░░░░░ ░░░░░░░ ${RESET}"
"${DARK_GRAY}░░░░ ░░░░░░ ░░░░░░ ${ORANGE}░░${RESET}"
"${DARK_GRAY}░░░░ ░░░░░░░ ${ORANGE}░░▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ░░░ ${ORANGE}░▒▒▒▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}▒▒▒░ ░▒▒▒▒▒▒▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}░▒▒▒▒▒ ▒▒▒▒▒░░ ▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}░░▒▒▒▒▒▒▒░░ ▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}░░░ ▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}▒▒▒▒${RESET}"
"${DARK_GRAY}░░░░ ${ORANGE}▒▒▒░${RESET}"
"${DARK_GRAY} ░░ ${ORANGE}░░ ${RESET}"
)
for i in {0..12}; do
echo -e "${TAB}${LOGO[i]} │${RESET} ${TEXT[i]}"
done
echo -e
fi
}
# ==========================================================
cleanup_corrupted_files() {
if [ -f "$CONFIG_FILE" ] && ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
echo "Cleaning up corrupted configuration file..."
rm -f "$CONFIG_FILE"
fi
}
# Cleanup function
cleanup() {
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
fi
}
# Set trap to ensure cleanup on exit
trap cleanup EXIT
# ==========================================================
check_existing_installation() {
# After the googletrans removal there is only one install variant.
# The function still distinguishes "installed" vs "not installed" so
# show_installation_options can pick the right banner.
if [ -f "$INSTALL_DIR/$MENU_SCRIPT" ]; then
# Quietly fix a corrupted config so the install can proceed.
if [ -f "$CONFIG_FILE" ] && ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
echo "Warning: Corrupted config file detected, removing..." >&2
rm -f "$CONFIG_FILE"
fi
echo "installed"
else
echo "none"
fi
}
update_config() {
local component="$1"
local status="$2"
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local tracked_components=("dialog" "curl" "jq" "git" "python3" "python3-pip" "proxmenux_monitor")
if [[ " ${tracked_components[@]} " =~ " ${component} " ]]; then
mkdir -p "$(dirname "$CONFIG_FILE")"
if [ ! -f "$CONFIG_FILE" ] || ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
echo '{}' > "$CONFIG_FILE"
fi
local tmp_file=$(mktemp)
if jq --arg comp "$component" --arg stat "$status" --arg time "$timestamp" \
'.[$comp] = {status: $stat, timestamp: $time}' "$CONFIG_FILE" > "$tmp_file" 2>/dev/null; then
mv "$tmp_file" "$CONFIG_FILE"
else
echo '{}' > "$CONFIG_FILE"
jq --arg comp "$component" --arg stat "$status" --arg time "$timestamp" \
'.[$comp] = {status: $stat, timestamp: $time}' "$CONFIG_FILE" > "$tmp_file" && mv "$tmp_file" "$CONFIG_FILE"
fi
[ -f "$tmp_file" ] && rm -f "$tmp_file"
fi
}
# apt output goes to a temporary file: shown only when the command fails, so
# the reader sees why. Non-interactive, because a hidden prompt would hang.
APT_ERROR_LOG=""
run_apt() {
APT_ERROR_LOG=$(mktemp /tmp/proxmenux-apt.XXXXXX)
if DEBIAN_FRONTEND=noninteractive apt-get "$@" >"$APT_ERROR_LOG" 2>&1; then
rm -f "$APT_ERROR_LOG"
APT_ERROR_LOG=""
return 0
fi
return 1
}
show_apt_error() {
[ -n "$APT_ERROR_LOG" ] && [ -f "$APT_ERROR_LOG" ] || return 0
tail -n 15 "$APT_ERROR_LOG" | sed 's/^/ /'
rm -f "$APT_ERROR_LOG"
APT_ERROR_LOG=""
}
discard_apt_error() {
[ -n "$APT_ERROR_LOG" ] && rm -f "$APT_ERROR_LOG"
APT_ERROR_LOG=""
}
# A Proxmox host without a subscription is refused by the enterprise
# repositories. Every other repository is refreshed, so that refresh is good.
apt_only_enterprise_refused() {
[ -n "$APT_ERROR_LOG" ] && [ -f "$APT_ERROR_LOG" ] || return 1
local failures
failures=$(grep -E '^(E: |Err:|W: Failed to fetch)' "$APT_ERROR_LOG") || return 1
! grep -qv 'enterprise\.proxmox\.com' <<<"$failures"
}
show_progress() {
local step="$1"
local total="$2"
local message="$3"
echo -e "\n${BOLD}${BL}${TAB}Installing ProxMenux: Step $step of $total${CL}"
echo
msg_info2 "$message"
}
select_language() {
if [ -f "$CONFIG_FILE" ] && jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
local existing_language=$(jq -r '.language // empty' "$CONFIG_FILE" 2>/dev/null)
if [[ -n "$existing_language" && "$existing_language" != "null" && "$existing_language" != "empty" ]]; then
LANGUAGE="$existing_language"
msg_ok "Using existing language configuration: $LANGUAGE"
return 0
fi
fi
LANGUAGE=$(whiptail --title "Select Language" --menu "Choose a language for the menu:" 20 60 12 \
"en" "English" \
"es" "Spanish" \
"fr" "French" \
"de" "German" \
"it" "Italian" \
"pt" "Portuguese" \
"sk" "Slovenčina" \
"sv" "Svenska" 3>&1 1>&2 2>&3)
if [ -z "$LANGUAGE" ]; then
msg_error "No language selected. Exiting."
exit 1
fi
mkdir -p "$(dirname "$CONFIG_FILE")"
if [ ! -f "$CONFIG_FILE" ] || ! jq empty "$CONFIG_FILE" >/dev/null 2>&1; then
echo '{}' > "$CONFIG_FILE"
fi
local tmp_file=$(mktemp)
if jq --arg lang "$LANGUAGE" '. + {language: $lang}' "$CONFIG_FILE" > "$tmp_file" 2>/dev/null; then
mv "$tmp_file" "$CONFIG_FILE"
else
echo "{\"language\": \"$LANGUAGE\"}" > "$CONFIG_FILE"
fi
[ -f "$tmp_file" ] && rm -f "$tmp_file"
msg_ok "Language set to: $LANGUAGE"
}
# Show installation confirmation for new installations
show_installation_confirmation() {
if whiptail --title "ProxMenux Installation" \
--yesno "ProxMenux will install:\n\n• dialog (interactive menus) - Official Debian package\n• curl (file downloads) - Official Debian package\n• jq (JSON processing) - Official Debian package\n• ProxMenux core files (/usr/local/share/proxmenux)\n• ProxMenux Monitor (Web dashboard on port 8008)\n• Pre-built translation files\n\nProceed with installation?" 20 70; then
return 0
else
return 1
fi
}
get_server_ip() {
local ip
# Try to get the primary IP address
ip=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+')
if [ -z "$ip" ]; then
# Fallback: get first non-loopback IP
ip=$(hostname -I | awk '{print $1}')
fi
if [ -z "$ip" ]; then
# Last resort: use localhost
ip="localhost"
fi
echo "$ip"
}
detect_latest_appimage() {
local appimage_dir="$TEMP_DIR/AppImage"
if [ ! -d "$appimage_dir" ]; then
return 1
fi
local latest_appimage=$(find "$appimage_dir" -name "ProxMenux-*.AppImage" -type f | sort -V | tail -1)
if [ -z "$latest_appimage" ]; then
return 1
fi
echo "$latest_appimage"
return 0
}
get_appimage_version() {
local appimage_path="$1"
local filename=$(basename "$appimage_path")
# Match any dotted number sequence + optional pre-release suffix
# (e.g. "-beta"). The previous `[0-9]+\.[0-9]+\.[0-9]+` was hardcoded
# to three segments and dropped both the fourth segment AND the
# `-beta` suffix on a name like `ProxMenux-1.2.1.2-beta.AppImage`.
local version=$(echo "$filename" | grep -oP 'ProxMenux-\K[0-9]+(?:\.[0-9]+)+(?:-[A-Za-z0-9]+)?')
echo "$version"
}
# ── AppImage runtime extraction ────────────────────────────
# Extract the AppImage's squashfs to a stable directory and run AppRun
# directly. Avoids the FUSE mount under /tmp/.mount_ProxMe<random>, which
# trips Wazuh rule 521 / rkhunter "Possible kernel level rootkit" alerts
# (issue #101) — those scanners flag any directory that appears in
# readdir() but is hidden from lstat(), which is exactly what AppImage's
# FUSE mount layer looks like to them. Running from a plain extracted
# directory has the same files but no FUSE indirection, so the false
# positive disappears.
extract_appimage_to_runtime_dir() {
local appimage_path="$1"
local target_runtime_dir="$2"
local tmp_extract_dir
tmp_extract_dir=$(mktemp -d /tmp/proxmenux-extract.XXXXXX) || return 1
msg_info "Extracting AppImage runtime to ${target_runtime_dir}..."
if ! ( cd "$tmp_extract_dir" && "$appimage_path" --appimage-extract >/dev/null 2>&1 ); then
msg_error "Failed to extract AppImage."
rm -rf "$tmp_extract_dir"
return 1
fi
if [ ! -x "$tmp_extract_dir/squashfs-root/AppRun" ]; then
msg_error "Extracted AppImage missing AppRun."
rm -rf "$tmp_extract_dir"
return 1
fi
rm -rf "${target_runtime_dir}.new"
mv "$tmp_extract_dir/squashfs-root" "${target_runtime_dir}.new"
rm -rf "$tmp_extract_dir"
if [ -d "$target_runtime_dir" ]; then
rm -rf "${target_runtime_dir}.old"
mv "$target_runtime_dir" "${target_runtime_dir}.old"
fi
mv "${target_runtime_dir}.new" "$target_runtime_dir"
rm -rf "${target_runtime_dir}.old"
rm -f "$appimage_path"
msg_ok "AppImage runtime extracted."
return 0
}
install_proxmenux_monitor() {
local appimage_source=$(detect_latest_appimage)
if [ -z "$appimage_source" ] || [ ! -f "$appimage_source" ]; then
msg_error "ProxMenux Monitor AppImage not found in $TEMP_DIR/AppImage/"
msg_warn "Please ensure the AppImage directory exists with ProxMenux-*.AppImage files."
update_config "proxmenux_monitor" "appimage_not_found"
return 1
fi
local appimage_version=$(get_appimage_version "$appimage_source")
if systemctl is-active --quiet proxmenux-monitor.service; then
systemctl stop proxmenux-monitor.service
fi
local service_exists=false
if [ -f "$MONITOR_SERVICE_FILE" ]; then
service_exists=true
fi
local sha256_file="$TEMP_DIR/AppImage/ProxMenux-Monitor.AppImage.sha256"
if [ -f "$sha256_file" ]; then
msg_info "Verifying AppImage integrity..."
local expected_hash=$(cat "$sha256_file" | grep -Eo '^[a-f0-9]+' | tr -d '\n')
local actual_hash=$(sha256sum "$appimage_source" | awk '{print $1}')
if [ "$expected_hash" != "$actual_hash" ]; then
msg_error "SHA256 verification failed! AppImage may be corrupted."
return 1
fi
msg_ok "SHA256 verification passed."
else
msg_warn "SHA256 checksum not available. Skipping verification."
fi
mkdir -p "$MONITOR_INSTALL_DIR"
local target_path="$MONITOR_INSTALL_DIR/ProxMenux-Monitor.AppImage"
cp "$appimage_source" "$target_path"
chmod +x "$target_path"
if ! extract_appimage_to_runtime_dir "$target_path" "$MONITOR_RUNTIME_DIR"; then
update_config "proxmenux_monitor" "extract_failed"
return 1
fi
msg_ok "ProxMenux Monitor v$appimage_version installed."
if [ "$service_exists" = false ]; then
return 0 # New installation - service needs to be created
else
# The v1.2.2 install layout extracts the AppImage into
# MONITOR_RUNTIME_DIR/ and runs AppRun out of that directory
# (`extract_appimage_to_runtime_dir` above), so the unit must
# point at AppRun — not at the bare AppImage. Existing users
# updating from v1.2.1.x stable still have a unit whose
# ExecStart targets `/usr/local/share/proxmenux/ProxMenux-Monitor.AppImage`
# which was fine when the AppImage was FUSE-mounted but breaks
# under PVE 9.x / Debian 13 (status=203/EXEC, GitHub issue #222).
# Rewrite the unit on every update — idempotent for users
# whose unit is already correct.
_proxmenux_rewrite_monitor_unit_for_apprun
systemctl start proxmenux-monitor.service
sleep 2
if systemctl is-active --quiet proxmenux-monitor.service; then
update_config "proxmenux_monitor" "updated"
return 2 # Update successful
else
msg_warn "Service failed to restart. Check: journalctl -u proxmenux-monitor"
update_config "proxmenux_monitor" "failed"
return 1
fi
fi
}
# Idempotent rewriter of the proxmenux-monitor unit file. Used by the
# update path in `install_proxmenux_monitor` so that existing
# installations updated to v1.2.2+ get their ExecStart corrected to
# point at the extracted AppRun even when the unit already exists.
# Mirrors `create_monitor_service`'s unit body so both code paths
# converge on the same file content. Returns 0 always; failures are
# logged so the surrounding flow can still attempt the start and
# report a more accurate failure to the user.
_proxmenux_rewrite_monitor_unit_for_apprun() {
local exec_path="$MONITOR_RUNTIME_DIR/AppRun"
if [ -f "$TEMP_DIR/systemd/proxmenux-monitor.service" ]; then
sed "s|ExecStart=.*|ExecStart=$exec_path|g" \
"$TEMP_DIR/systemd/proxmenux-monitor.service" > "$MONITOR_SERVICE_FILE"
else
cat > "$MONITOR_SERVICE_FILE" << EOF
[Unit]
Description=ProxMenux Monitor - Web Dashboard
After=network.target pve-cluster.service
[Service]
Type=simple
User=root
WorkingDirectory=$MONITOR_INSTALL_DIR
ExecStart=$exec_path
Restart=on-failure
RestartSec=10
Environment="PORT=$MONITOR_PORT"
[Install]
WantedBy=multi-user.target
EOF
fi
systemctl daemon-reload
return 0
}
create_monitor_service() {
local exec_path="$MONITOR_RUNTIME_DIR/AppRun"
if [ -f "$TEMP_DIR/systemd/proxmenux-monitor.service" ]; then
sed "s|ExecStart=.*|ExecStart=$exec_path|g" \
"$TEMP_DIR/systemd/proxmenux-monitor.service" > "$MONITOR_SERVICE_FILE"
msg_ok "Using service file from repository."
else
cat > "$MONITOR_SERVICE_FILE" << EOF
[Unit]
Description=ProxMenux Monitor - Web Dashboard
After=network.target pve-cluster.service
[Service]
Type=simple
User=root
WorkingDirectory=$MONITOR_INSTALL_DIR
ExecStart=$exec_path
Restart=on-failure
RestartSec=10
Environment="PORT=$MONITOR_PORT"
[Install]
WantedBy=multi-user.target
EOF
msg_ok "Created default service file."
fi
systemctl daemon-reload
systemctl enable proxmenux-monitor.service > /dev/null 2>&1
systemctl start proxmenux-monitor.service > /dev/null 2>&1
sleep 3
if systemctl is-active --quiet proxmenux-monitor.service; then
msg_ok "ProxMenux Monitor service started successfully."
update_config "proxmenux_monitor" "installed"
return 0
else
msg_warn "ProxMenux Monitor service failed to start."
msg_info2 "Check logs with: journalctl -u proxmenux-monitor -n 20"
msg_info2 "Check status with: systemctl status proxmenux-monitor"
update_config "proxmenux_monitor" "failed"
return 1
fi
}
install_normal_version() {
local total_steps=6
local current_step=1
# Translations now live as pre-generated JSON files under lang/, so
# asking the language up front is the right place — every install is
# multilingual-capable and the user picks once.
show_progress $current_step $total_steps "Language selection"
select_language
((current_step++))
# Purge the legacy googletrans virtualenv if a previous install left it
# behind. The new translate flow has no runtime Python/googletrans
# dependency — the venv is dead weight on disk now.
if [[ -d "$LEGACY_VENV_PATH" ]]; then
msg_info "Removing legacy translation virtualenv at $LEGACY_VENV_PATH..."
rm -rf "$LEGACY_VENV_PATH"
msg_ok "Legacy translation virtualenv removed."
fi
show_progress $current_step $total_steps "Installing basic dependencies."
msg_info "Refreshing apt cache..."
if run_apt update -y; then
msg_ok "apt cache refreshed."
elif apt_only_enterprise_refused; then
discard_apt_error
msg_ok "apt cache refreshed. The Proxmox enterprise repositories need a subscription and were skipped."
else
msg_warn "apt cache refresh failed; checking available packages."
show_apt_error
fi
msg_info "Installing jq..."
if ! command -v jq > /dev/null 2>&1; then
if run_apt install -y jq && command -v jq > /dev/null 2>&1; then
update_config "jq" "installed"
else
discard_apt_error
local jq_url="https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64"
if wget -q -O /usr/local/bin/jq "$jq_url" 2>/dev/null && chmod +x /usr/local/bin/jq; then
if command -v jq > /dev/null 2>&1; then
update_config "jq" "installed_from_github"
else
msg_error "Failed to install jq. Please install it manually."
update_config "jq" "failed"
return 1
fi
else
msg_error "Failed to install jq from both APT and GitHub. Please install it manually."
update_config "jq" "failed"
return 1
fi
fi
else
update_config "jq" "already_installed"
fi
msg_ok "jq ready."
BASIC_DEPS=("dialog" "curl" "git")
for pkg in "${BASIC_DEPS[@]}"; do
msg_info "Installing $pkg..."
# dpkg-query for the EXACT package — `dpkg -l | grep -qw python3`
# falsely matches `python3-pip`. Issue #205.
if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "ok installed"; then
if run_apt install -y "$pkg"; then
update_config "$pkg" "installed"
else
msg_error "Failed to install $pkg. Please install it manually."
show_apt_error
update_config "$pkg" "failed"
return 1
fi
else
update_config "$pkg" "already_installed"
fi
msg_ok "$pkg ready."
done
if ! command -v git > /dev/null 2>&1; then
msg_info "Installing git (required to clone the ProxMenux repository)."
if [ -z "${APT_UPDATED:-}" ]; then
apt-get update -y > /dev/null 2>&1 || true
APT_UPDATED=1
fi
if ! apt-get install -y git > /dev/null 2>&1; then
msg_error "Failed to install git. Please run 'apt-get install git' manually and rerun the installer."
update_config "git" "failed"
return 1
fi
if ! command -v git > /dev/null 2>&1; then
msg_error "Git is still not available after installation. Aborting to avoid a broken setup."
update_config "git" "failed"
return 1
fi
update_config "git" "installed"
else
update_config "git" "already_installed"
fi
msg_ok "jq, dialog, curl and git installed successfully."
((current_step++))
show_progress $current_step $total_steps "Install ProxMenux repository"
msg_info "Cloning ProxMenux repository."
if ! git clone --depth 1 "$REPO_URL" "$TEMP_DIR" 2>/dev/null; then
msg_error "Failed to clone repository from $REPO_URL"
exit 1
fi
msg_ok "Repository cloned successfully."
cd "$TEMP_DIR"
((current_step++))
show_progress $current_step $total_steps "Creating directories and configuration"
mkdir -p "$BASE_DIR"
mkdir -p "$INSTALL_DIR"
if [ ! -f "$CONFIG_FILE" ]; then
echo '{}' > "$CONFIG_FILE"
fi
msg_ok "Directories and configuration created."
((current_step++))
show_progress $current_step $total_steps "Copying necessary files"
cp "./scripts/utils.sh" "$UTILS_FILE"
# Atomic install of /usr/local/bin/menu: stage to .new on the same
# filesystem then mv. This protects any reader that happens to open
# the file mid-install from seeing a partial/half-written script
# (the suspected root cause of the post-1.2.2-update reports:
# "menu: line 138 syntax error near unexpected token `$REMOTE_VERSION`")
cp "./menu" "$INSTALL_DIR/${MENU_SCRIPT}.new"
mv -f "$INSTALL_DIR/${MENU_SCRIPT}.new" "$INSTALL_DIR/$MENU_SCRIPT"
cp "./version.txt" "$LOCAL_VERSION_FILE"
cp "./install_proxmenux.sh" "$BASE_DIR/install_proxmenux.sh"
# Pre-built translation cache. The runtime translate() in utils.sh
# reads $BASE_DIR/lang/<lang>.json — these files ship with the repo
# (one per supported language) so the install ends up multilingual
# without any runtime download or Python dependency. Refresh the
# whole dir on every install so a language that was renamed or
# dropped upstream disappears here too.
if [ -d "./lang" ]; then
rm -rf "$BASE_DIR/lang"
mkdir -p "$BASE_DIR/lang"
cp -r "./lang/"* "$BASE_DIR/lang/" 2>/dev/null || true
fi
# A user that previously rode the beta train and then switched back
# to stable would still have a leftover beta_version.txt under
# $BASE_DIR, which makes the `menu` update check (check_updates_beta)
# offer a "Beta update available" prompt on top of the legitimate
# stable one. Clearing the marker on every stable install/update
# keeps the stable install honestly stable — if the user opts into
# the beta program again, the beta installer will recreate the file.
rm -f "$BASE_DIR/beta_version.txt"
# Wipe the scripts tree before copying so any file removed upstream
# (renamed, consolidated, deprecated) disappears from the user install.
# Only $BASE_DIR/scripts/ is cleared; config.json,
# components_status.json, version.txt, monitor.db, smart/, oci/ and
# the AppImage live outside this path and are preserved.
rm -rf "$BASE_DIR/scripts"
mkdir -p "$BASE_DIR/scripts"
cp -r "./scripts/"* "$BASE_DIR/scripts/"
# Only .sh files need the executable bit. Applying +x recursively would
# also flag README.md, .json, .py etc. as executable for no reason.
find "$BASE_DIR/scripts" -type f -name '*.sh' -exec chmod +x {} +
# Register the base dependencies the installer put in place. They are
# installed in the dependency step, before this clone delivers
# pmx_journal.sh, so the journal cannot capture them as they happen —
# this records them once the engine is available. The recording side of
# pmx_journal.sh is pure bash and needs nothing else installed.
if [ -f "$BASE_DIR/scripts/global/pmx_journal.sh" ]; then
# shellcheck source=/dev/null
source "$BASE_DIR/scripts/global/pmx_journal.sh"
pmx_journal_context "install_proxmenux" "1.0" "install_proxmenux"
pmx_record_install "dialog jq curl git" "1.0"
fi
# The OCI engine is replaced on every install; instance records and
# addon state stored next to it in $BASE_DIR/oci are preserved.
if [ -d "./oci" ]; then
rm -rf "$BASE_DIR/oci/engine"
mkdir -p "$BASE_DIR/oci/engine"
cp -r "./oci/"* "$BASE_DIR/oci/engine/"
find "$BASE_DIR/oci/engine" -type f -name '*.sh' -exec chmod +x {} +
# Multi-container applications already installed take the new start hook.
bash "$BASE_DIR/oci/engine/remote/stack_dependency_hook.sh" --refresh >/dev/null 2>&1 || true
systemctl try-restart proxmenux-oci-watchdog.service >/dev/null 2>&1 || true
fi
chmod +x "$BASE_DIR/install_proxmenux.sh"
msg_ok "Necessary files created."
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
((current_step++))
show_progress $current_step $total_steps "Installing ProxMenux Monitor"
install_proxmenux_monitor
local monitor_status=$?
if [ $monitor_status -eq 0 ]; then
create_monitor_service
fi
msg_ok "ProxMenux installation completed successfully."
}
show_installation_options() {
local current_install_type
current_install_type=$(check_existing_installation)
# Translation Version is gone — translations now ship as pre-built
# JSON files in lang/. There is only one install path, so this
# function just shows the confirmation dialog for new installs and
# then returns. Existing installs go straight through (they already
# consented to update via the menu).
INSTALL_TYPE="1"
if [ "$current_install_type" = "none" ]; then
if ! show_installation_confirmation "$INSTALL_TYPE"; then
show_proxmenux_logo
msg_warn "Installation cancelled."
exit 1
fi
fi
}
install_proxmenux() {
if [[ "${UPDATE_MODE:-0}" == "1" ]]; then
# Update path: the user already accepted "Update now?" in the
# menu. Hand off to the freshly-installed menu binary at the end
# (exec, see below) so no shell ever returns to a half-written
# /usr/local/bin/menu — the new copy is the only thing parsed.
show_proxmenux_logo
msg_title "Updating ProxMenux"
install_normal_version || return $?
else
show_installation_options
show_proxmenux_logo
msg_title "Installing ProxMenux"
install_normal_version || return $?
fi
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
# ── Legacy gpu-guard hookscript auto-cleanup ──────────────
# Previous ProxMenux versions attached a hookscript to VMs/LXCs with GPU
# passthrough; that reference in the guest .conf broke backup/restore to
# hosts without the snippet. The hookscript system has been removed.
# This silently purges any leftover references and the snippet file.
# Idempotent: does nothing on hosts that never had the legacy hook.
if [ -x "$LOCAL_SCRIPTS/global/cleanup_gpu_hookscripts.sh" ]; then
bash "$LOCAL_SCRIPTS/global/cleanup_gpu_hookscripts.sh" || true
fi
# UPDATE_MODE used to `exec "$INSTALL_DIR/$MENU_SCRIPT"` here to
# auto-relaunch the freshly-installed menu. That produced visible
# "line: syntax" errors when bash tried to read the just-rewritten
# /usr/local/bin/menu (or a shared utils.sh sourced by it) under
# its feet. Fall through to the standard "installed successfully"
# end-of-run message instead — the operator types `menu` when
# ready and the terminal is stable by then.
#
# `change_release_channel` in scripts/menus/config_menu.sh is
# unaffected: it invokes the installer without `--update`
# (UPDATE_MODE=0) so it never went through this branch, and its
# "return to config menu" behaviour is preserved.
msg_title "ProxMenux has been installed successfully"
if systemctl is-active --quiet proxmenux-monitor.service; then
local server_ip=$(get_server_ip)
echo -e "${GN}🌐 ProxMenux Monitor activated${CL}: ${BL}http://${server_ip}:${MONITOR_PORT}${CL}"
echo
fi
echo -ne "${GN}"
type_text "To run ProxMenux, simply execute this command in the console or terminal:"
echo -e "${YWB} menu${CL}"
echo
# -------
exit 0
}
# Parse CLI flags before anything else so install_proxmenux() can
# branch on UPDATE_MODE without re-reading "$@".
if [[ "${1:-}" == "--update" ]]; then
UPDATE_MODE=1
fi
if [ "$(id -u)" -ne 0 ]; then
msg_error "This script must be run as root."
exit 1
fi
cleanup_corrupted_files
install_proxmenux || exit $?