Got the terminal working
This commit is contained in:
90
package-lock.json
generated
90
package-lock.json
generated
@@ -16,13 +16,20 @@
|
||||
"@trpc/react-query": "^11.0.0",
|
||||
"@trpc/server": "^11.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@xterm/addon-attach": "^0.11.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"next": "^15.2.3",
|
||||
"node-pty": "^1.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
"simple-git": "^3.28.0",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"superjson": "^2.2.1",
|
||||
"ws": "^8.18.3",
|
||||
"xterm": "^5.3.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -2115,6 +2122,39 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@xterm/addon-attach": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-attach/-/addon-attach-0.11.0.tgz",
|
||||
"integrity": "sha512-JboCN0QAY6ZLY/SSB/Zl2cQ5zW1Eh4X3fH7BnuR1NB7xGRhzbqU2Npmpiw/3zFlxDaU88vtKzok44JKi2L2V2Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-web-links": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz",
|
||||
"integrity": "sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/xterm": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
@@ -2155,6 +2195,18 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
@@ -5039,6 +5091,12 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
|
||||
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -5167,6 +5225,16 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-pty": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
|
||||
"integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nan": "^2.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
|
||||
@@ -6320,6 +6388,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-bom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
@@ -6877,6 +6960,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xterm": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
|
||||
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
|
||||
"deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
|
||||
@@ -29,13 +29,20 @@
|
||||
"@trpc/react-query": "^11.0.0",
|
||||
"@trpc/server": "^11.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@xterm/addon-attach": "^0.11.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"next": "^15.2.3",
|
||||
"node-pty": "^1.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
"simple-git": "^3.28.0",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"superjson": "^2.2.1",
|
||||
"ws": "^8.18.3",
|
||||
"xterm": "^5.3.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
801
scripts/core/build.func
Executable file
801
scripts/core/build.func
Executable file
@@ -0,0 +1,801 @@
|
||||
# Copyright (c) 2021-2025 michelroegl-brunner
|
||||
# Author: michelroegl-brunner
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
|
||||
variables() {
|
||||
NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
|
||||
var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
|
||||
INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
|
||||
PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
|
||||
METHOD="default" # sets the METHOD variable to "default", used for the API call.
|
||||
CT_TYPE=${var_unprivileged:-$CT_TYPE}
|
||||
}
|
||||
|
||||
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/core.func"
|
||||
|
||||
# This function enables error handling in the script by setting options and defining a trap for the ERR signal.
|
||||
catch_errors() {
|
||||
set -Eeo pipefail
|
||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||
}
|
||||
|
||||
# This function is called when an error occurs. It receives the exit code, line number, and command that caused the error, and displays an error message.
|
||||
error_handler() {
|
||||
|
||||
printf "\e[?25h"
|
||||
local exit_code="$?"
|
||||
local line_number="$1"
|
||||
local command="$2"
|
||||
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
|
||||
|
||||
echo -e "\n$error_message\n"
|
||||
}
|
||||
|
||||
# Check if the shell is using bash
|
||||
shell_check() {
|
||||
if [[ "$(basename "$SHELL")" != "bash" ]]; then
|
||||
clear
|
||||
msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell."
|
||||
echo -e "\nExiting..."
|
||||
sleep 2
|
||||
exit
|
||||
fi
|
||||
}
|
||||
|
||||
# Run as root only
|
||||
root_check() {
|
||||
if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
|
||||
clear
|
||||
msg_error "Please run this script as root."
|
||||
echo -e "\nExiting..."
|
||||
sleep 2
|
||||
exit
|
||||
fi
|
||||
}
|
||||
|
||||
# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported.
|
||||
# Supported: Proxmox VE 8.0.x – 8.9.x and 9.0 (NOT 9.1+)
|
||||
pve_check() {
|
||||
local PVE_VER
|
||||
PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
|
||||
|
||||
# Check for Proxmox VE 8.x: allow 8.0–8.9
|
||||
if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then
|
||||
local MINOR="${BASH_REMATCH[1]}"
|
||||
if ((MINOR < 0 || MINOR > 9)); then
|
||||
msg_error "This version of Proxmox VE is not supported."
|
||||
msg_error "Supported: Proxmox VE version 8.0 – 8.9"
|
||||
exit 1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for Proxmox VE 9.x: allow ONLY 9.0
|
||||
if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
|
||||
local MINOR="${BASH_REMATCH[1]}"
|
||||
if ((MINOR != 0)); then
|
||||
msg_error "This version of Proxmox VE is not yet supported."
|
||||
msg_error "Supported: Proxmox VE version 9.0"
|
||||
exit 1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# All other unsupported versions
|
||||
msg_error "This version of Proxmox VE is not supported."
|
||||
msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# When a node is running tens of containers, it's possible to exceed the kernel's cryptographic key storage allocations.
|
||||
# These are tuneable, so verify if the currently deployment is approaching the limits, advise the user on how to tune the limits, and exit the script.
|
||||
# https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html
|
||||
maxkeys_check() {
|
||||
# Read kernel parameters
|
||||
per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
|
||||
per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)
|
||||
|
||||
# Exit if kernel parameters are unavailable
|
||||
if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
|
||||
echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fetch key usage for user ID 100000 (typical for containers)
|
||||
used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
|
||||
used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)
|
||||
|
||||
# Calculate thresholds and suggested new limits
|
||||
threshold_keys=$((per_user_maxkeys - 100))
|
||||
threshold_bytes=$((per_user_maxbytes - 1000))
|
||||
new_limit_keys=$((per_user_maxkeys * 2))
|
||||
new_limit_bytes=$((per_user_maxbytes * 2))
|
||||
|
||||
# Check if key or byte usage is near limits
|
||||
failure=0
|
||||
if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
|
||||
echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}"
|
||||
echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
|
||||
failure=1
|
||||
fi
|
||||
if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
|
||||
echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}"
|
||||
echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
|
||||
failure=1
|
||||
fi
|
||||
|
||||
# Provide next steps if issues are detected
|
||||
if [[ "$failure" -eq 1 ]]; then
|
||||
echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}"
|
||||
}
|
||||
|
||||
# This function checks the system architecture and exits if it's not "amd64".
|
||||
arch_check() {
|
||||
if [ "$(dpkg --print-architecture)" != "amd64" ]; then
|
||||
echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
|
||||
echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
|
||||
echo -e "Exiting..."
|
||||
sleep 2
|
||||
exit
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get the current IP address based on the distribution
|
||||
get_current_ip() {
|
||||
if [ -f /etc/os-release ]; then
|
||||
# Check for Debian/Ubuntu (uses hostname -I)
|
||||
if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then
|
||||
CURRENT_IP=$(hostname -I | awk '{print $1}')
|
||||
# Check for Alpine (uses ip command)
|
||||
elif grep -q 'ID=alpine' /etc/os-release; then
|
||||
CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1)
|
||||
else
|
||||
CURRENT_IP="Unknown"
|
||||
fi
|
||||
fi
|
||||
echo "$CURRENT_IP"
|
||||
}
|
||||
|
||||
# Function to update the IP address in the MOTD file
|
||||
update_motd_ip() {
|
||||
MOTD_FILE="/etc/motd"
|
||||
|
||||
if [ -f "$MOTD_FILE" ]; then
|
||||
# Remove existing IP Address lines to prevent duplication
|
||||
sed -i '/IP Address:/d' "$MOTD_FILE"
|
||||
|
||||
IP=$(get_current_ip)
|
||||
# Add the new IP address
|
||||
echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# This function checks if the script is running through SSH and prompts the user to confirm if they want to proceed or exit.
|
||||
ssh_check() {
|
||||
if [ -n "${SSH_CLIENT:+x}" ]; then
|
||||
if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's advisable to utilize the Proxmox shell rather than SSH, as there may be potential complications with variable retrieval. Proceed using SSH?" 10 72; then
|
||||
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox --title "Proceed using SSH" "You've chosen to proceed using SSH. If any issues arise, please run the script in the Proxmox shell before creating a repository issue." 10 72
|
||||
else
|
||||
clear
|
||||
echo "Exiting due to SSH usage. Please consider using the Proxmox shell."
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
base_settings() {
|
||||
# Default Settings
|
||||
CT_TYPE=${var_unprivileged:-"1"}
|
||||
DISK_SIZE=${var_disk:-"4"}
|
||||
CORE_COUNT=${var_cpu:-"1"}
|
||||
RAM_SIZE=${var_ram:-"1024"}
|
||||
VERBOSE=${var_verbose:-"${1:-no}"}
|
||||
PW=${var_pw:-""}
|
||||
CT_ID=${var_ctid:-$NEXTID}
|
||||
HN=${var_hostname:-$NSAPP}
|
||||
BRG=${var_brg:-"vmbr0"}
|
||||
NET=${var_net:-"dhcp"}
|
||||
IPV6_METHOD=${var_ipv6_method:-"none"}
|
||||
IPV6_STATIC=${var_ipv6_static:-""}
|
||||
GATE=${var_gateway:-""}
|
||||
APT_CACHER=${var_apt_cacher:-""}
|
||||
APT_CACHER_IP=${var_apt_cacher_ip:-""}
|
||||
MTU=${var_mtu:-""}
|
||||
SD=${var_storage:-""}
|
||||
NS=${var_ns:-""}
|
||||
MAC=${var_mac:-""}
|
||||
VLAN=${var_vlan:-""}
|
||||
SSH=${var_ssh:-"no"}
|
||||
SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""}
|
||||
UDHCPC_FIX=${var_udhcpc_fix:-""}
|
||||
TAGS="community-script;${var_tags:-}"
|
||||
ENABLE_FUSE=${var_fuse:-"${1:-no}"}
|
||||
ENABLE_TUN=${var_tun:-"${1:-no}"}
|
||||
|
||||
# Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
|
||||
if [ -z "$var_os" ]; then
|
||||
var_os="debian"
|
||||
fi
|
||||
if [ -z "$var_version" ]; then
|
||||
var_version="12"
|
||||
fi
|
||||
}
|
||||
|
||||
write_config() {
|
||||
mkdir -p /opt/community-scripts
|
||||
# This function writes the configuration to a file.
|
||||
if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Write configfile" --yesno "Do you want to write the selections to a config file?" 10 60; then
|
||||
FILEPATH="/opt/community-scripts/${NSAPP}.conf"
|
||||
[[ "$GATE" =~ ",gw=" ]] && local GATE="${GATE##,gw=}"
|
||||
|
||||
# Strip prefixes from parameters for config file storage
|
||||
local SD_VALUE="${SD}"
|
||||
local NS_VALUE="${NS}"
|
||||
local MAC_VALUE="${MAC}"
|
||||
local VLAN_VALUE="${VLAN}"
|
||||
[[ "$SD" =~ ^-searchdomain= ]] && SD_VALUE="${SD#-searchdomain=}"
|
||||
[[ "$NS" =~ ^-nameserver= ]] && NS_VALUE="${NS#-nameserver=}"
|
||||
[[ "$MAC" =~ ^,hwaddr= ]] && MAC_VALUE="${MAC#,hwaddr=}"
|
||||
[[ "$VLAN" =~ ^,tag= ]] && VLAN_VALUE="${VLAN#,tag=}"
|
||||
|
||||
if [[ ! -f $FILEPATH ]]; then
|
||||
cat <<EOF >"$FILEPATH"
|
||||
# ${NSAPP} Configuration File
|
||||
# Generated on $(date)
|
||||
|
||||
CT_TYPE="${CT_TYPE}"
|
||||
DISK_SIZE="${DISK_SIZE}"
|
||||
CORE_COUNT="${CORE_COUNT}"
|
||||
RAM_SIZE="${RAM_SIZE}"
|
||||
VERBOSE="${VERBOSE}"
|
||||
PW="${PW##-password }"
|
||||
#CT_ID=$NEXTID
|
||||
HN="${HN}"
|
||||
BRG="${BRG}"
|
||||
NET="${NET}"
|
||||
IPV6_METHOD="${IPV6_METHOD:-none}"
|
||||
# Set this only if using "IPV6_METHOD=static"
|
||||
#IPV6STATIC="fd00::1234/64"
|
||||
|
||||
GATE="${GATE:-none}"
|
||||
APT_CACHER_IP="${APT_CACHER_IP:-none}"
|
||||
MTU="${MTU:-1500}"
|
||||
SD="${SD_VALUE:-none}"
|
||||
NS="${NS_VALUE:-none}"
|
||||
MAC="${MAC_VALUE:-none}"
|
||||
VLAN="${VLAN_VALUE:-none}"
|
||||
SSH="${SSH}"
|
||||
SSH_AUTHORIZED_KEY="${SSH_AUTHORIZED_KEY}"
|
||||
TAGS="${TAGS:-none}"
|
||||
ENABLE_FUSE="$ENABLE_FUSE"
|
||||
ENABLE_TUN="$ENABLE_TUN"
|
||||
EOF
|
||||
|
||||
echo -e "${INFO}${BOLD}${GN}Writing configuration to ${FILEPATH}${CL}"
|
||||
else
|
||||
echo -e "${INFO}${BOLD}${RD}Configuration file already exists at ${FILEPATH}${CL}"
|
||||
if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Overwrite configfile" --yesno "Do you want to overwrite the existing config file?" 10 60; then
|
||||
rm -f "$FILEPATH"
|
||||
cat <<EOF >"$FILEPATH"
|
||||
# ${NSAPP} Configuration File
|
||||
# Generated on $(date)
|
||||
|
||||
CT_TYPE="${CT_TYPE}"
|
||||
DISK_SIZE="${DISK_SIZE}"
|
||||
CORE_COUNT="${CORE_COUNT}"
|
||||
RAM_SIZE="${RAM_SIZE}"
|
||||
VERBOSE="${VERBOSE}"
|
||||
PW="${PW##-password }"
|
||||
#CT_ID=$NEXTID
|
||||
HN="${HN}"
|
||||
BRG="${BRG}"
|
||||
NET="${NET}"
|
||||
IPV6_METHOD="${IPV6_METHOD:-none}"
|
||||
|
||||
# Set this only if using "IPV6_METHOD=static"
|
||||
#IPV6STATIC="fd00::1234/64"
|
||||
|
||||
GATE="${GATE:-none}"
|
||||
APT_CACHER_IP="${APT_CACHER_IP:-none}"
|
||||
MTU="${MTU:-1500}"
|
||||
SD="${SD_VALUE:-none}"
|
||||
NS="${NS_VALUE:-none}"
|
||||
MAC="${MAC_VALUE:-none}"
|
||||
VLAN="${VLAN_VALUE:-none}"
|
||||
SSH="${SSH}"
|
||||
SSH_AUTHORIZED_KEY="${SSH_AUTHORIZED_KEY}"
|
||||
TAGS="${TAGS:-none}"
|
||||
ENABLE_FUSE="$ENABLE_FUSE"
|
||||
ENABLE_TUN="$ENABLE_TUN"
|
||||
EOF
|
||||
echo -e "${INFO}${BOLD}${GN}Writing configuration to ${FILEPATH}${CL}"
|
||||
else
|
||||
echo -e "${INFO}${BOLD}${RD}Configuration file not overwritten${CL}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# This function displays the default values for various settings.
|
||||
echo_default() {
|
||||
# Convert CT_TYPE to description
|
||||
CT_TYPE_DESC="Unprivileged"
|
||||
if [ "$CT_TYPE" -eq 0 ]; then
|
||||
CT_TYPE_DESC="Privileged"
|
||||
fi
|
||||
|
||||
# Output the selected values with icons
|
||||
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
|
||||
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
|
||||
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
|
||||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
|
||||
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
|
||||
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
|
||||
if [ "$VERBOSE" == "yes" ]; then
|
||||
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
|
||||
fi
|
||||
echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
|
||||
echo -e " "
|
||||
}
|
||||
|
||||
# This function is called when the user decides to exit the script. It clears the screen and displays an exit message.
|
||||
exit_script() {
|
||||
clear
|
||||
echo -e "\n${CROSS}${RD}User exited script${CL}\n"
|
||||
exit
|
||||
}
|
||||
|
||||
|
||||
install_script() {
|
||||
pve_check
|
||||
shell_check
|
||||
root_check
|
||||
arch_check
|
||||
#ssh_check
|
||||
maxkeys_check
|
||||
|
||||
|
||||
|
||||
|
||||
if systemctl is-active -q ping-instances.service; then
|
||||
systemctl -q stop ping-instances.service
|
||||
fi
|
||||
NEXTID=$(pvesh get /cluster/nextid)
|
||||
timezone=$(cat /etc/timezone)
|
||||
#header_info
|
||||
echo "TEST"
|
||||
while true; do
|
||||
|
||||
TMP_CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||||
--title "SETTINGS" \
|
||||
--menu "Choose an option:" 20 60 6 \
|
||||
"1" "Default Settings" \
|
||||
"2" "Default Settings (with verbose)" \
|
||||
"3" "Advanced Settings" \
|
||||
"4" "Exit" \
|
||||
--default-item "1" 3>&1 1>&2 2>&3) || true
|
||||
|
||||
if [ -z "$TMP_CHOICE" ]; then
|
||||
echo -e "\n${CROSS}${RD}Menu canceled. Exiting script.${CL}\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CHOICE="$TMP_CHOICE"
|
||||
|
||||
case $CHOICE in
|
||||
1)
|
||||
header_info
|
||||
echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
|
||||
VERBOSE="no"
|
||||
METHOD="default"
|
||||
base_settings "$VERBOSE"
|
||||
echo_default
|
||||
break
|
||||
;;
|
||||
2)
|
||||
header_info
|
||||
echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME (${VERBOSE_CROPPED}Verbose)${CL}"
|
||||
VERBOSE="yes"
|
||||
METHOD="default"
|
||||
base_settings "$VERBOSE"
|
||||
echo_default
|
||||
break
|
||||
;;
|
||||
3)
|
||||
header_info
|
||||
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}"
|
||||
METHOD="advanced"
|
||||
base_settings
|
||||
advanced_settings
|
||||
break
|
||||
;;
|
||||
4)
|
||||
echo -e "\n${CROSS}${RD}Script terminated. Have a great day!${CL}\n"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "\n${CROSS}${RD}Invalid option, please try again.${CL}\n"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
check_container_resources() {
|
||||
# Check actual RAM & Cores
|
||||
current_ram=$(free -m | awk 'NR==2{print $2}')
|
||||
current_cpu=$(nproc)
|
||||
|
||||
# Check whether the current RAM is less than the required RAM or the CPU cores are less than required
|
||||
if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
|
||||
echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}"
|
||||
echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
|
||||
echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? [y/N] "
|
||||
read -r prompt
|
||||
if [[ ! "${prompt,,}" =~ ^(y|yes)$ ]]; then
|
||||
echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e ""
|
||||
fi
|
||||
}
|
||||
|
||||
check_container_storage() {
|
||||
# Check if the /boot partition is more than 80% full
|
||||
total_size=$(df /boot --output=size | tail -n 1)
|
||||
local used_size=$(df /boot --output=used | tail -n 1)
|
||||
usage=$((100 * used_size / total_size))
|
||||
if ((usage > 80)); then
|
||||
# Prompt the user for confirmation to continue
|
||||
echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
|
||||
echo -ne "Continue anyway? [y/N] "
|
||||
read -r prompt
|
||||
if [[ ! "${prompt,,}" =~ ^(y|yes)$ ]]; then
|
||||
echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
start() {
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func)
|
||||
if command -v pveversion >/dev/null 2>&1; then
|
||||
install_script
|
||||
else
|
||||
CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
|
||||
"Support/Update functions for ${APP} LXC. Choose an option:" \
|
||||
12 60 3 \
|
||||
"1" "YES (Silent Mode)" \
|
||||
"2" "YES (Verbose Mode)" \
|
||||
"3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3)
|
||||
|
||||
case "$CHOICE" in
|
||||
1)
|
||||
VERBOSE="no"
|
||||
set_std_mode
|
||||
;;
|
||||
2)
|
||||
VERBOSE="yes"
|
||||
set_std_mode
|
||||
;;
|
||||
3)
|
||||
clear
|
||||
exit_script
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
update_script
|
||||
fi
|
||||
}
|
||||
|
||||
# This function collects user settings and integrates all the collected information.
|
||||
build_container() {
|
||||
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
|
||||
|
||||
NET_STRING="-net0 name=eth0,bridge=$BRG$MAC,ip=$NET$GATE$VLAN$MTU"
|
||||
case "$IPV6_METHOD" in
|
||||
auto) NET_STRING="$NET_STRING,ip6=auto" ;;
|
||||
dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
|
||||
static)
|
||||
NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
|
||||
[ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
|
||||
;;
|
||||
none) ;;
|
||||
esac
|
||||
if [ "$CT_TYPE" == "1" ]; then
|
||||
FEATURES="keyctl=1,nesting=1"
|
||||
else
|
||||
FEATURES="nesting=1"
|
||||
fi
|
||||
|
||||
if [ "$ENABLE_FUSE" == "yes" ]; then
|
||||
FEATURES="$FEATURES,fuse=1"
|
||||
fi
|
||||
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
pushd "$TEMP_DIR" >/dev/null
|
||||
if [ "$var_os" == "alpine" ]; then
|
||||
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func)"
|
||||
else
|
||||
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func)"
|
||||
fi
|
||||
|
||||
export DIAGNOSTICS="$DIAGNOSTICS"
|
||||
export RANDOM_UUID="$RANDOM_UUID"
|
||||
export CACHER="$APT_CACHER"
|
||||
export CACHER_IP="$APT_CACHER_IP"
|
||||
export tz="$timezone"
|
||||
export APPLICATION="$APP"
|
||||
export app="$NSAPP"
|
||||
export PASSWORD="$PW"
|
||||
export VERBOSE="$VERBOSE"
|
||||
export SSH_ROOT="${SSH}"
|
||||
export SSH_AUTHORIZED_KEY
|
||||
export CTID="$CT_ID"
|
||||
export CTTYPE="$CT_TYPE"
|
||||
export ENABLE_FUSE="$ENABLE_FUSE"
|
||||
export ENABLE_TUN="$ENABLE_TUN"
|
||||
export PCT_OSTYPE="$var_os"
|
||||
export PCT_OSVERSION="$var_version"
|
||||
export PCT_DISK_SIZE="$DISK_SIZE"
|
||||
export PCT_OPTIONS="
|
||||
-features $FEATURES
|
||||
-hostname $HN
|
||||
-tags $TAGS
|
||||
$SD
|
||||
$NS
|
||||
$NET_STRING
|
||||
-onboot 1
|
||||
-cores $CORE_COUNT
|
||||
-memory $RAM_SIZE
|
||||
-unprivileged $CT_TYPE
|
||||
$PW
|
||||
"
|
||||
# This executes create_lxc.sh and creates the container and .conf file
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/create_lxc.sh)" $?
|
||||
|
||||
LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
|
||||
|
||||
# USB passthrough for privileged LXC (CT_TYPE=0)
|
||||
if [ "$CT_TYPE" == "0" ]; then
|
||||
cat <<EOF >>"$LXC_CONFIG"
|
||||
# USB passthrough
|
||||
lxc.cgroup2.devices.allow: a
|
||||
lxc.cap.drop:
|
||||
lxc.cgroup2.devices.allow: c 188:* rwm
|
||||
lxc.cgroup2.devices.allow: c 189:* rwm
|
||||
lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
|
||||
lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
|
||||
lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
|
||||
lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
|
||||
lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
|
||||
EOF
|
||||
fi
|
||||
|
||||
# VAAPI passthrough for privileged containers or known apps
|
||||
VAAPI_APPS=(
|
||||
"immich"
|
||||
"Channels"
|
||||
"Emby"
|
||||
"ErsatzTV"
|
||||
"Frigate"
|
||||
"Jellyfin"
|
||||
"Plex"
|
||||
"Scrypted"
|
||||
"Tdarr"
|
||||
"Unmanic"
|
||||
"Ollama"
|
||||
"FileFlows"
|
||||
"Open WebUI"
|
||||
)
|
||||
|
||||
is_vaapi_app=false
|
||||
for vaapi_app in "${VAAPI_APPS[@]}"; do
|
||||
if [[ "$APP" == "$vaapi_app" ]]; then
|
||||
is_vaapi_app=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if ([ "$CT_TYPE" == "0" ] || [ "$is_vaapi_app" == "true" ]) &&
|
||||
([[ -e /dev/dri/renderD128 ]] || [[ -e /dev/dri/card0 ]] || [[ -e /dev/fb0 ]]); then
|
||||
|
||||
echo ""
|
||||
msg_custom "⚙️ " "\e[96m" "Configuring VAAPI passthrough for LXC container"
|
||||
if [ "$CT_TYPE" != "0" ]; then
|
||||
msg_custom "⚠️ " "\e[33m" "Container is unprivileged – VAAPI passthrough may not work without additional host configuration (e.g., idmap)."
|
||||
fi
|
||||
msg_custom "ℹ️ " "\e[96m" "VAAPI enables GPU hardware acceleration (e.g., for video transcoding in Jellyfin or Plex)."
|
||||
echo ""
|
||||
read -rp "➤ Automatically mount all available VAAPI devices? [Y/n]: " VAAPI_ALL
|
||||
|
||||
if [[ "$VAAPI_ALL" =~ ^[Yy]$|^$ ]]; then
|
||||
if [ "$CT_TYPE" == "0" ]; then
|
||||
# PRV Container → alles zulässig
|
||||
[[ -e /dev/dri/renderD128 ]] && {
|
||||
echo "lxc.cgroup2.devices.allow: c 226:128 rwm" >>"$LXC_CONFIG"
|
||||
echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG"
|
||||
}
|
||||
[[ -e /dev/dri/card0 ]] && {
|
||||
echo "lxc.cgroup2.devices.allow: c 226:0 rwm" >>"$LXC_CONFIG"
|
||||
echo "lxc.mount.entry: /dev/dri/card0 dev/dri/card0 none bind,optional,create=file" >>"$LXC_CONFIG"
|
||||
}
|
||||
[[ -e /dev/fb0 ]] && {
|
||||
echo "lxc.cgroup2.devices.allow: c 29:0 rwm" >>"$LXC_CONFIG"
|
||||
echo "lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file" >>"$LXC_CONFIG"
|
||||
}
|
||||
[[ -d /dev/dri ]] && {
|
||||
echo "lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG"
|
||||
}
|
||||
else
|
||||
# UNPRV Container → nur devX für UI
|
||||
[[ -e /dev/dri/card0 ]] && echo "dev0: /dev/dri/card0,gid=44" >>"$LXC_CONFIG"
|
||||
[[ -e /dev/dri/card1 ]] && echo "dev0: /dev/dri/card1,gid=44" >>"$LXC_CONFIG"
|
||||
[[ -e /dev/dri/renderD128 ]] && echo "dev1: /dev/dri/renderD128,gid=104" >>"$LXC_CONFIG"
|
||||
fi
|
||||
fi
|
||||
|
||||
fi
|
||||
if [ "$CT_TYPE" == "1" ] && [ "$is_vaapi_app" == "true" ]; then
|
||||
if [[ -e /dev/dri/card0 ]]; then
|
||||
echo "dev0: /dev/dri/card0,gid=44" >>"$LXC_CONFIG"
|
||||
elif [[ -e /dev/dri/card1 ]]; then
|
||||
echo "dev0: /dev/dri/card1,gid=44" >>"$LXC_CONFIG"
|
||||
fi
|
||||
if [[ -e /dev/dri/renderD128 ]]; then
|
||||
echo "dev1: /dev/dri/renderD128,gid=104" >>"$LXC_CONFIG"
|
||||
fi
|
||||
fi
|
||||
|
||||
# TUN device passthrough
|
||||
if [ "$ENABLE_TUN" == "yes" ]; then
|
||||
cat <<EOF >>"$LXC_CONFIG"
|
||||
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||||
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
|
||||
EOF
|
||||
fi
|
||||
|
||||
# This starts the container and executes <app>-install.sh
|
||||
msg_info "Starting LXC Container"
|
||||
pct start "$CTID"
|
||||
|
||||
# wait for status 'running'
|
||||
for i in {1..10}; do
|
||||
if pct status "$CTID" | grep -q "status: running"; then
|
||||
msg_ok "Started LXC Container"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
if [ "$i" -eq 10 ]; then
|
||||
msg_error "LXC Container did not reach running state"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$var_os" != "alpine" ]; then
|
||||
msg_info "Waiting for network in LXC container"
|
||||
for i in {1..10}; do
|
||||
# 1. Primary check: ICMP ping (fastest, but may be blocked by ISP/firewall)
|
||||
if pct exec "$CTID" -- ping -c1 -W1 deb.debian.org >/dev/null 2>&1; then
|
||||
msg_ok "Network in LXC is reachable (ping)"
|
||||
break
|
||||
fi
|
||||
# Wait and retry if not reachable yet
|
||||
if [ "$i" -lt 10 ]; then
|
||||
msg_warn "No network in LXC yet (try $i/10) – waiting..."
|
||||
sleep 3
|
||||
else
|
||||
# After 10 unsuccessful ping attempts, try HTTP connectivity via wget as fallback
|
||||
msg_warn "Ping failed 10 times. Trying HTTP connectivity check (wget) as fallback..."
|
||||
if pct exec "$CTID" -- wget -q --spider http://deb.debian.org; then
|
||||
msg_ok "Network in LXC is reachable (wget fallback)"
|
||||
else
|
||||
msg_error "No network in LXC after all checks."
|
||||
read -r -p "Set fallback DNS (1.1.1.1/8.8.8.8)? [y/N]: " choice
|
||||
case "$choice" in
|
||||
[yY]*)
|
||||
pct set "$CTID" --nameserver 1.1.1.1
|
||||
pct set "$CTID" --nameserver 8.8.8.8
|
||||
# Final attempt with wget after DNS change
|
||||
if pct exec "$CTID" -- wget -q --spider http://deb.debian.org; then
|
||||
msg_ok "Network reachable after DNS fallback"
|
||||
else
|
||||
msg_error "Still no network/DNS in LXC! Aborting customization."
|
||||
exit_script
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
msg_error "Aborted by user – no DNS fallback set."
|
||||
exit_script
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
msg_info "Customizing LXC Container"
|
||||
: "${tz:=Etc/UTC}"
|
||||
if [ "$var_os" == "alpine" ]; then
|
||||
sleep 3
|
||||
pct exec "$CTID" -- /bin/sh -c 'cat <<EOF >/etc/apk/repositories
|
||||
http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
|
||||
http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
|
||||
EOF'
|
||||
pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null"
|
||||
else
|
||||
sleep 3
|
||||
pct exec "$CTID" -- bash -c "sed -i '/$LANG/ s/^# //' /etc/locale.gen"
|
||||
pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
|
||||
echo LANG=\$locale_line >/etc/default/locale && \
|
||||
locale-gen >/dev/null && \
|
||||
export LANG=\$locale_line"
|
||||
|
||||
if [[ -z "${tz:-}" ]]; then
|
||||
tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC")
|
||||
fi
|
||||
if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
|
||||
pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime"
|
||||
else
|
||||
msg_warn "Skipping timezone setup – zone '$tz' not found in container"
|
||||
fi
|
||||
|
||||
pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null"
|
||||
fi
|
||||
msg_ok "Customized LXC Container"
|
||||
|
||||
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh)"
|
||||
}
|
||||
|
||||
# This function sets the description of the container.
|
||||
description() {
|
||||
IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
|
||||
|
||||
# Generate LXC Description
|
||||
DESCRIPTION=$(
|
||||
cat <<EOF
|
||||
<div align='center'>
|
||||
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
|
||||
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
|
||||
</a>
|
||||
|
||||
<h2 style='font-size: 24px; margin: 20px 0;'>${APP} LXC</h2>
|
||||
|
||||
<p style='margin: 16px 0;'>
|
||||
<a href='https://ko-fi.com/community_scripts' target='_blank' rel='noopener noreferrer'>
|
||||
<img src='https://img.shields.io/badge/☕-Buy us a coffee-blue' alt='spend Coffee' />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<span style='margin: 0 10px;'>
|
||||
<i class="fa fa-github fa-fw" style="color: #f5f5f5;"></i>
|
||||
<a href='https://github.com/community-scripts/ProxmoxVE' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>GitHub</a>
|
||||
</span>
|
||||
<span style='margin: 0 10px;'>
|
||||
<i class="fa fa-comments fa-fw" style="color: #f5f5f5;"></i>
|
||||
<a href='https://github.com/community-scripts/ProxmoxVE/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Discussions</a>
|
||||
</span>
|
||||
<span style='margin: 0 10px;'>
|
||||
<i class="fa fa-exclamation-circle fa-fw" style="color: #f5f5f5;"></i>
|
||||
<a href='https://github.com/community-scripts/ProxmoxVE/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Issues</a>
|
||||
</span>
|
||||
</div>
|
||||
EOF
|
||||
)
|
||||
|
||||
# Set Description in LXC
|
||||
pct set "$CTID" -description "$DESCRIPTION"
|
||||
|
||||
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
|
||||
systemctl start ping-instances.service
|
||||
fi
|
||||
|
||||
}
|
||||
409
scripts/core/core.func
Normal file
409
scripts/core/core.func
Normal file
@@ -0,0 +1,409 @@
|
||||
# Copyright (c) 2021-2025 community-scripts ORG
|
||||
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Loads core utility groups once (colors, formatting, icons, defaults).
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
[[ -n "${_CORE_FUNC_LOADED:-}" ]] && return
|
||||
_CORE_FUNC_LOADED=1
|
||||
|
||||
load_functions() {
|
||||
[[ -n "${__FUNCTIONS_LOADED:-}" ]] && return
|
||||
__FUNCTIONS_LOADED=1
|
||||
color
|
||||
formatting
|
||||
icons
|
||||
default_vars
|
||||
set_std_mode
|
||||
# add more
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Error & Signal Handling – robust, universal, subshell-safe
|
||||
# ============================================================================
|
||||
|
||||
_tool_error_hint() {
|
||||
local cmd="$1"
|
||||
local code="$2"
|
||||
case "$cmd" in
|
||||
curl)
|
||||
case "$code" in
|
||||
6) echo "Curl: Could not resolve host (DNS problem)" ;;
|
||||
7) echo "Curl: Failed to connect to host (connection refused)" ;;
|
||||
22) echo "Curl: HTTP error (404/403 etc)" ;;
|
||||
28) echo "Curl: Operation timeout" ;;
|
||||
*) echo "Curl: Unknown error ($code)" ;;
|
||||
esac
|
||||
;;
|
||||
wget)
|
||||
echo "Wget failed – URL unreachable or permission denied"
|
||||
;;
|
||||
systemctl)
|
||||
echo "Systemd unit failure – check service name and permissions"
|
||||
;;
|
||||
jq)
|
||||
echo "jq parse error – malformed JSON or missing key"
|
||||
;;
|
||||
mariadb | mysql)
|
||||
echo "MySQL/MariaDB command failed – check credentials or DB"
|
||||
;;
|
||||
unzip)
|
||||
echo "unzip failed – corrupt file or missing permission"
|
||||
;;
|
||||
tar)
|
||||
echo "tar failed – invalid format or missing binary"
|
||||
;;
|
||||
node | npm | pnpm | yarn)
|
||||
echo "Node tool failed – check version compatibility or package.json"
|
||||
;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
catch_errors() {
|
||||
set -Eeuo pipefail
|
||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Sets ANSI color codes used for styled terminal output.
|
||||
# ------------------------------------------------------------------------------
|
||||
color() {
|
||||
YW=$(echo "\033[33m")
|
||||
YWB=$'\e[93m'
|
||||
BL=$(echo "\033[36m")
|
||||
RD=$(echo "\033[01;31m")
|
||||
BGN=$(echo "\033[4;92m")
|
||||
GN=$(echo "\033[1;92m")
|
||||
DGN=$(echo "\033[32m")
|
||||
CL=$(echo "\033[m")
|
||||
}
|
||||
|
||||
# Special for spinner and colorized output via printf
|
||||
color_spinner() {
|
||||
CS_YW=$'\033[33m'
|
||||
CS_YWB=$'\033[93m'
|
||||
CS_CL=$'\033[m'
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Defines formatting helpers like tab, bold, and line reset sequences.
|
||||
# ------------------------------------------------------------------------------
|
||||
formatting() {
|
||||
BFR="\\r\\033[K"
|
||||
BOLD=$(echo "\033[1m")
|
||||
HOLD=" "
|
||||
TAB=" "
|
||||
TAB3=" "
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Sets symbolic icons used throughout user feedback and prompts.
|
||||
# ------------------------------------------------------------------------------
|
||||
icons() {
|
||||
CM="${TAB}✔️${TAB}"
|
||||
CROSS="${TAB}✖️${TAB}"
|
||||
DNSOK="✔️ "
|
||||
DNSFAIL="${TAB}✖️${TAB}"
|
||||
INFO="${TAB}💡${TAB}${CL}"
|
||||
OS="${TAB}🖥️${TAB}${CL}"
|
||||
OSVERSION="${TAB}🌟${TAB}${CL}"
|
||||
CONTAINERTYPE="${TAB}📦${TAB}${CL}"
|
||||
DISKSIZE="${TAB}💾${TAB}${CL}"
|
||||
CPUCORE="${TAB}🧠${TAB}${CL}"
|
||||
RAMSIZE="${TAB}🛠️${TAB}${CL}"
|
||||
SEARCH="${TAB}🔍${TAB}${CL}"
|
||||
VERBOSE_CROPPED="🔍${TAB}"
|
||||
VERIFYPW="${TAB}🔐${TAB}${CL}"
|
||||
CONTAINERID="${TAB}🆔${TAB}${CL}"
|
||||
HOSTNAME="${TAB}🏠${TAB}${CL}"
|
||||
BRIDGE="${TAB}🌉${TAB}${CL}"
|
||||
NETWORK="${TAB}📡${TAB}${CL}"
|
||||
GATEWAY="${TAB}🌐${TAB}${CL}"
|
||||
DISABLEIPV6="${TAB}🚫${TAB}${CL}"
|
||||
DEFAULT="${TAB}⚙️${TAB}${CL}"
|
||||
MACADDRESS="${TAB}🔗${TAB}${CL}"
|
||||
VLANTAG="${TAB}🏷️${TAB}${CL}"
|
||||
ROOTSSH="${TAB}🔑${TAB}${CL}"
|
||||
CREATING="${TAB}🚀${TAB}${CL}"
|
||||
ADVANCED="${TAB}🧩${TAB}${CL}"
|
||||
FUSE="${TAB}🗂️${TAB}${CL}"
|
||||
HOURGLASS="${TAB}⏳${TAB}"
|
||||
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Sets default retry and wait variables used for system actions.
|
||||
# ------------------------------------------------------------------------------
|
||||
default_vars() {
|
||||
RETRY_NUM=10
|
||||
RETRY_EVERY=3
|
||||
i=$RETRY_NUM
|
||||
#[[ "${VAR_OS:-}" == "unknown" ]]
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Sets default verbose mode for script and os execution.
|
||||
# ------------------------------------------------------------------------------
|
||||
set_std_mode() {
|
||||
if [ "${VERBOSE:-no}" = "yes" ]; then
|
||||
STD=""
|
||||
else
|
||||
STD="silent"
|
||||
fi
|
||||
}
|
||||
|
||||
# Silent execution function
|
||||
silent() {
|
||||
"$@" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to download & save header files
|
||||
get_header() {
|
||||
local app_name=$(echo "${APP,,}" | tr -d ' ')
|
||||
local app_type=${APP_TYPE:-ct}
|
||||
local header_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/${app_type}/headers/${app_name}"
|
||||
local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}"
|
||||
|
||||
mkdir -p "$(dirname "$local_header_path")"
|
||||
|
||||
if [ ! -s "$local_header_path" ]; then
|
||||
if ! curl -fsSL "$header_url" -o "$local_header_path"; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
cat "$local_header_path" 2>/dev/null || true
|
||||
}
|
||||
|
||||
header_info() {
|
||||
local app_name=$(echo "${APP,,}" | tr -d ' ')
|
||||
local header_content
|
||||
|
||||
header_content=$(get_header "$app_name") || header_content=""
|
||||
|
||||
clear
|
||||
local term_width
|
||||
term_width=$(tput cols 2>/dev/null || echo 120)
|
||||
|
||||
if [ -n "$header_content" ]; then
|
||||
echo "$header_content"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_tput() {
|
||||
if ! command -v tput >/dev/null 2>&1; then
|
||||
if grep -qi 'alpine' /etc/os-release; then
|
||||
apk add --no-cache ncurses >/dev/null 2>&1
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update -qq >/dev/null
|
||||
apt-get install -y -qq ncurses-bin >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
is_alpine() {
|
||||
local os_id="${var_os:-${PCT_OSTYPE:-}}"
|
||||
|
||||
if [[ -z "$os_id" && -f /etc/os-release ]]; then
|
||||
os_id="$(
|
||||
. /etc/os-release 2>/dev/null
|
||||
echo "${ID:-}"
|
||||
)"
|
||||
fi
|
||||
|
||||
[[ "$os_id" == "alpine" ]]
|
||||
}
|
||||
|
||||
is_verbose_mode() {
|
||||
local verbose="${VERBOSE:-${var_verbose:-no}}"
|
||||
local tty_status
|
||||
if [[ -t 2 ]]; then
|
||||
tty_status="interactive"
|
||||
else
|
||||
tty_status="not-a-tty"
|
||||
fi
|
||||
[[ "$verbose" != "no" || ! -t 2 ]]
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Handles specific curl error codes and displays descriptive messages.
|
||||
# ------------------------------------------------------------------------------
|
||||
__curl_err_handler() {
|
||||
local exit_code="$1"
|
||||
local target="$2"
|
||||
local curl_msg="$3"
|
||||
|
||||
case $exit_code in
|
||||
1) msg_error "Unsupported protocol: $target" ;;
|
||||
2) msg_error "Curl init failed: $target" ;;
|
||||
3) msg_error "Malformed URL: $target" ;;
|
||||
5) msg_error "Proxy resolution failed: $target" ;;
|
||||
6) msg_error "Host resolution failed: $target" ;;
|
||||
7) msg_error "Connection failed: $target" ;;
|
||||
9) msg_error "Access denied: $target" ;;
|
||||
18) msg_error "Partial file transfer: $target" ;;
|
||||
22) msg_error "HTTP error (e.g. 400/404): $target" ;;
|
||||
23) msg_error "Write error on local system: $target" ;;
|
||||
26) msg_error "Read error from local file: $target" ;;
|
||||
28) msg_error "Timeout: $target" ;;
|
||||
35) msg_error "SSL connect error: $target" ;;
|
||||
47) msg_error "Too many redirects: $target" ;;
|
||||
51) msg_error "SSL cert verify failed: $target" ;;
|
||||
52) msg_error "Empty server response: $target" ;;
|
||||
55) msg_error "Send error: $target" ;;
|
||||
56) msg_error "Receive error: $target" ;;
|
||||
60) msg_error "SSL CA not trusted: $target" ;;
|
||||
67) msg_error "Login denied by server: $target" ;;
|
||||
78) msg_error "Remote file not found (404): $target" ;;
|
||||
*) msg_error "Curl failed with code $exit_code: $target" ;;
|
||||
esac
|
||||
|
||||
[[ -n "$curl_msg" ]] && printf "%s\n" "$curl_msg" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
fatal() {
|
||||
msg_error "$1"
|
||||
kill -INT $$
|
||||
}
|
||||
|
||||
spinner() {
|
||||
local chars=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
|
||||
local i=0
|
||||
while true; do
|
||||
local index=$((i++ % ${#chars[@]}))
|
||||
printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${SPINNER_MSG:-}${CS_CL}"
|
||||
sleep 0.1
|
||||
done
|
||||
}
|
||||
|
||||
clear_line() {
|
||||
tput cr 2>/dev/null || echo -en "\r"
|
||||
tput el 2>/dev/null || echo -en "\033[K"
|
||||
}
|
||||
|
||||
stop_spinner() {
|
||||
local pid="${SPINNER_PID:-}"
|
||||
[[ -z "$pid" && -f /tmp/.spinner.pid ]] && pid=$(</tmp/.spinner.pid)
|
||||
|
||||
if [[ -n "$pid" && "$pid" =~ ^[0-9]+$ ]]; then
|
||||
if kill "$pid" 2>/dev/null; then
|
||||
sleep 0.05
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
wait "$pid" 2>/dev/null || true
|
||||
fi
|
||||
rm -f /tmp/.spinner.pid
|
||||
fi
|
||||
|
||||
unset SPINNER_PID SPINNER_MSG
|
||||
stty sane 2>/dev/null || true
|
||||
}
|
||||
|
||||
msg_info() {
|
||||
local msg="$1"
|
||||
[[ -z "$msg" ]] && return
|
||||
|
||||
if ! declare -p MSG_INFO_SHOWN &>/dev/null || ! declare -A MSG_INFO_SHOWN &>/dev/null; then
|
||||
declare -gA MSG_INFO_SHOWN=()
|
||||
fi
|
||||
[[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]] && return
|
||||
MSG_INFO_SHOWN["$msg"]=1
|
||||
|
||||
stop_spinner
|
||||
SPINNER_MSG="$msg"
|
||||
|
||||
if is_verbose_mode || is_alpine; then
|
||||
local HOURGLASS="${TAB}⏳${TAB}"
|
||||
printf "\r\e[2K%s %b" "$HOURGLASS" "${YW}${msg}${CL}" >&2
|
||||
return
|
||||
fi
|
||||
|
||||
color_spinner
|
||||
spinner &
|
||||
SPINNER_PID=$!
|
||||
echo "$SPINNER_PID" >/tmp/.spinner.pid
|
||||
disown "$SPINNER_PID" 2>/dev/null || true
|
||||
}
|
||||
|
||||
msg_ok() {
|
||||
local msg="$1"
|
||||
[[ -z "$msg" ]] && return
|
||||
stop_spinner
|
||||
clear_line
|
||||
printf "%s %b\n" "$CM" "${GN}${msg}${CL}" >&2
|
||||
unset MSG_INFO_SHOWN["$msg"]
|
||||
}
|
||||
|
||||
msg_error() {
|
||||
stop_spinner
|
||||
local msg="$1"
|
||||
echo -e "${BFR:-} ${CROSS:-✖️} ${RD}${msg}${CL}"
|
||||
}
|
||||
|
||||
msg_warn() {
|
||||
stop_spinner
|
||||
local msg="$1"
|
||||
echo -e "${BFR:-} ${INFO:-ℹ️} ${YWB}${msg}${CL}"
|
||||
}
|
||||
|
||||
msg_custom() {
|
||||
local symbol="${1:-"[*]"}"
|
||||
local color="${2:-"\e[36m"}"
|
||||
local msg="${3:-}"
|
||||
[[ -z "$msg" ]] && return
|
||||
stop_spinner
|
||||
echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}"
|
||||
}
|
||||
|
||||
run_container_safe() {
|
||||
local ct="$1"
|
||||
shift
|
||||
local cmd="$*"
|
||||
|
||||
lxc-attach -n "$ct" -- bash -euo pipefail -c "
|
||||
trap 'echo Aborted in container; exit 130' SIGINT SIGTERM
|
||||
$cmd
|
||||
" || __handle_general_error "lxc-attach to CT $ct"
|
||||
}
|
||||
|
||||
check_or_create_swap() {
|
||||
msg_info "Checking for active swap"
|
||||
|
||||
if swapon --noheadings --show | grep -q 'swap'; then
|
||||
msg_ok "Swap is active"
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg_error "No active swap detected"
|
||||
|
||||
read -p "Do you want to create a swap file? [y/N]: " create_swap
|
||||
create_swap="${create_swap,,}" # to lowercase
|
||||
|
||||
if [[ "$create_swap" != "y" && "$create_swap" != "yes" ]]; then
|
||||
msg_info "Skipping swap file creation"
|
||||
return 1
|
||||
fi
|
||||
|
||||
read -p "Enter swap size in MB (e.g., 2048 for 2GB): " swap_size_mb
|
||||
if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then
|
||||
msg_error "Invalid size input. Aborting."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local swap_file="/swapfile"
|
||||
|
||||
msg_info "Creating ${swap_size_mb}MB swap file at $swap_file"
|
||||
if dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" status=progress &&
|
||||
chmod 600 "$swap_file" &&
|
||||
mkswap "$swap_file" &&
|
||||
swapon "$swap_file"; then
|
||||
msg_ok "Swap file created and activated successfully"
|
||||
else
|
||||
msg_error "Failed to create or activate swap"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
trap 'stop_spinner' EXIT INT TERM
|
||||
206
scripts/core/install.func
Executable file
206
scripts/core/install.func
Executable file
@@ -0,0 +1,206 @@
|
||||
# Copyright (c) 2021-2025 michelroegl-brunner
|
||||
# Author: michelroegl-brunner
|
||||
# License: MIT
|
||||
# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2
|
||||
apt-get update >/dev/null 2>&1
|
||||
apt-get install -y curl >/dev/null 2>&1
|
||||
fi
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/core.func"
|
||||
load_functions
|
||||
# This function enables IPv6 if it's not disabled and sets verbose mode
|
||||
verb_ip6() {
|
||||
set_std_mode # Set STD mode based on VERBOSE
|
||||
|
||||
if [ "$DISABLEIPV6" == "yes" ]; then
|
||||
echo "net.ipv6.conf.all.disable_ipv6 = 1" >>/etc/sysctl.conf
|
||||
$STD sysctl -p
|
||||
fi
|
||||
}
|
||||
|
||||
# This function sets error handling options and defines the error_handler function to handle errors
|
||||
catch_errors() {
|
||||
set -Eeuo pipefail
|
||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||
}
|
||||
|
||||
# This function handles errors
|
||||
error_handler() {
|
||||
printf "\e[?25h"
|
||||
local exit_code="$?"
|
||||
local line_number="$1"
|
||||
local command="$2"
|
||||
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
|
||||
echo -e "\n$error_message"
|
||||
if [[ "$line_number" -eq 51 ]]; then
|
||||
echo -e "The silent function has suppressed the error, run the script with verbose mode enabled, which will provide more detailed output.\n"
|
||||
post_update_to_api "failed" "No error message, script ran in silent mode"
|
||||
else
|
||||
post_update_to_api "failed" "${command}"
|
||||
fi
|
||||
}
|
||||
|
||||
# This function sets up the Container OS by generating the locale, setting the timezone, and checking the network connection
|
||||
setting_up_container() {
|
||||
msg_info "Setting up Container OS"
|
||||
for ((i = RETRY_NUM; i > 0; i--)); do
|
||||
if [ "$(hostname -I)" != "" ]; then
|
||||
break
|
||||
fi
|
||||
echo 1>&2 -en "${CROSS}${RD} No Network! "
|
||||
sleep $RETRY_EVERY
|
||||
done
|
||||
if [ "$(hostname -I)" = "" ]; then
|
||||
echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}"
|
||||
echo -e "${NETWORK}Check Network Settings"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
|
||||
systemctl disable -q --now systemd-networkd-wait-online.service
|
||||
msg_ok "Set up Container OS"
|
||||
#msg_custom "${CM}" "${GN}" "Network Connected: ${BL}$(hostname -I)"
|
||||
msg_ok "Network Connected: ${BL}$(hostname -I)"
|
||||
}
|
||||
|
||||
# This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected
|
||||
# This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected
|
||||
network_check() {
|
||||
set +e
|
||||
trap - ERR
|
||||
ipv4_connected=false
|
||||
ipv6_connected=false
|
||||
sleep 1
|
||||
|
||||
# Check IPv4 connectivity to Google, Cloudflare & Quad9 DNS servers.
|
||||
if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then
|
||||
msg_ok "IPv4 Internet Connected"
|
||||
ipv4_connected=true
|
||||
else
|
||||
msg_error "IPv4 Internet Not Connected"
|
||||
fi
|
||||
|
||||
# Check IPv6 connectivity to Google, Cloudflare & Quad9 DNS servers.
|
||||
if ping6 -c 1 -W 1 2606:4700:4700::1111 &>/dev/null || ping6 -c 1 -W 1 2001:4860:4860::8888 &>/dev/null || ping6 -c 1 -W 1 2620:fe::fe &>/dev/null; then
|
||||
msg_ok "IPv6 Internet Connected"
|
||||
ipv6_connected=true
|
||||
else
|
||||
msg_error "IPv6 Internet Not Connected"
|
||||
fi
|
||||
|
||||
# If both IPv4 and IPv6 checks fail, prompt the user
|
||||
if [[ $ipv4_connected == false && $ipv6_connected == false ]]; then
|
||||
read -r -p "No Internet detected, would you like to continue anyway? <y/N> " prompt
|
||||
if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
|
||||
echo -e "${INFO}${RD}Expect Issues Without Internet${CL}"
|
||||
else
|
||||
echo -e "${NETWORK}Check Network Settings"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# DNS resolution checks for GitHub-related domains (IPv4 and/or IPv6)
|
||||
GIT_HOSTS=("github.com" "raw.githubusercontent.com" "api.github.com" "git.community-scripts.org")
|
||||
GIT_STATUS="Git DNS:"
|
||||
DNS_FAILED=false
|
||||
|
||||
for HOST in "${GIT_HOSTS[@]}"; do
|
||||
RESOLVEDIP=$(getent hosts "$HOST" | awk '{ print $1 }' | grep -E '(^([0-9]{1,3}\.){3}[0-9]{1,3}$)|(^[a-fA-F0-9:]+$)' | head -n1)
|
||||
if [[ -z "$RESOLVEDIP" ]]; then
|
||||
GIT_STATUS+="$HOST:($DNSFAIL)"
|
||||
DNS_FAILED=true
|
||||
else
|
||||
GIT_STATUS+=" $HOST:($DNSOK)"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$DNS_FAILED" == true ]]; then
|
||||
fatal "$GIT_STATUS"
|
||||
else
|
||||
msg_ok "$GIT_STATUS"
|
||||
fi
|
||||
|
||||
set -e
|
||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||
}
|
||||
|
||||
# This function updates the Container OS by running apt-get update and upgrade
|
||||
update_os() {
|
||||
msg_info "Updating Container OS"
|
||||
if [[ "$CACHER" == "yes" ]]; then
|
||||
echo 'Acquire::http::Proxy-Auto-Detect "/usr/local/bin/apt-proxy-detect.sh";' >/etc/apt/apt.conf.d/00aptproxy
|
||||
cat <<EOF >/usr/local/bin/apt-proxy-detect.sh
|
||||
#!/bin/bash
|
||||
if nc -w1 -z "${CACHER_IP}" 3142; then
|
||||
echo -n "http://${CACHER_IP}:3142"
|
||||
else
|
||||
echo -n "DIRECT"
|
||||
fi
|
||||
EOF
|
||||
chmod +x /usr/local/bin/apt-proxy-detect.sh
|
||||
fi
|
||||
$STD apt-get update
|
||||
$STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade
|
||||
rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
|
||||
msg_ok "Updated Container OS"
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/tools.func")
|
||||
}
|
||||
|
||||
# This function modifies the message of the day (motd) and SSH settings
|
||||
motd_ssh() {
|
||||
# Set terminal to 256-color mode
|
||||
grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc
|
||||
|
||||
# Get OS information (Debian / Ubuntu)
|
||||
if [ -f "/etc/os-release" ]; then
|
||||
OS_NAME=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"')
|
||||
OS_VERSION=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')
|
||||
elif [ -f "/etc/debian_version" ]; then
|
||||
OS_NAME="Debian"
|
||||
OS_VERSION=$(cat /etc/debian_version)
|
||||
fi
|
||||
|
||||
PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
|
||||
echo "echo -e \"\"" >"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE"
|
||||
echo "echo \"\"" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE"
|
||||
echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE"
|
||||
|
||||
# Disable default MOTD scripts
|
||||
chmod -x /etc/update-motd.d/*
|
||||
|
||||
if [[ "${SSH_ROOT}" == "yes" ]]; then
|
||||
sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config
|
||||
systemctl restart sshd
|
||||
fi
|
||||
}
|
||||
|
||||
# This function customizes the container by modifying the getty service and enabling auto-login for the root user
|
||||
customize() {
|
||||
if [[ "$PASSWORD" == "" ]]; then
|
||||
msg_info "Customizing Container"
|
||||
GETTY_OVERRIDE="/etc/systemd/system/container-getty@1.service.d/override.conf"
|
||||
mkdir -p $(dirname $GETTY_OVERRIDE)
|
||||
cat <<EOF >$GETTY_OVERRIDE
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 \$TERM
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl restart $(basename $(dirname $GETTY_OVERRIDE) | sed 's/\.d//')
|
||||
msg_ok "Customized Container"
|
||||
fi
|
||||
|
||||
|
||||
if [[ -n "${SSH_AUTHORIZED_KEY}" ]]; then
|
||||
mkdir -p /root/.ssh
|
||||
echo "${SSH_AUTHORIZED_KEY}" >/root/.ssh/authorized_keys
|
||||
chmod 700 /root/.ssh
|
||||
chmod 600 /root/.ssh/authorized_keys
|
||||
fi
|
||||
}
|
||||
43
scripts/ct/debian.sh
Executable file
43
scripts/ct/debian.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
source "$SCRIPT_DIR/../core/build.func"
|
||||
# Copyright (c) 2021-2025 tteck
|
||||
# Author: tteck (tteckster)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://www.debian.org/
|
||||
|
||||
APP="Debian"
|
||||
var_tags="${var_tags:-os}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-512}"
|
||||
var_disk="${var_disk:-2}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-12}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
if [[ ! -d /var ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
msg_info "Updating $APP LXC"
|
||||
$STD apt-get update
|
||||
$STD apt-get -y upgrade
|
||||
msg_ok "Updated $APP LXC"
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed Successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Demo script for PVE Scripts Local Management
|
||||
# This script demonstrates live output streaming
|
||||
|
||||
echo "🚀 Starting PVE Script Demo..."
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
echo "📋 System Information:"
|
||||
echo " - Hostname: $(hostname)"
|
||||
echo " - User: $(whoami)"
|
||||
echo " - Date: $(date)"
|
||||
echo " - Uptime: $(uptime)"
|
||||
echo ""
|
||||
|
||||
echo "🔧 Simulating Proxmox operations..."
|
||||
echo " - Checking Proxmox API connection..."
|
||||
sleep 2
|
||||
echo " ✅ API connection successful"
|
||||
echo ""
|
||||
|
||||
echo " - Listing VMs..."
|
||||
sleep 1
|
||||
echo " 📦 VM 100: Ubuntu Server 22.04 (running)"
|
||||
echo " 📦 VM 101: Windows Server 2022 (stopped)"
|
||||
echo " 📦 VM 102: Debian 12 (running)"
|
||||
echo ""
|
||||
|
||||
echo " - Checking storage..."
|
||||
sleep 1
|
||||
echo " 💾 Local storage: 500GB (200GB used)"
|
||||
echo " 💾 NFS storage: 2TB (800GB used)"
|
||||
echo ""
|
||||
|
||||
echo " - Checking cluster status..."
|
||||
sleep 1
|
||||
echo " 🏗️ Node: pve-01 (online)"
|
||||
echo " 🏗️ Node: pve-02 (online)"
|
||||
echo " 🏗️ Node: pve-03 (maintenance)"
|
||||
echo ""
|
||||
|
||||
echo "🎯 Demo completed successfully!"
|
||||
echo "================================"
|
||||
echo "This script ran for demonstration purposes."
|
||||
echo "In a real scenario, this would perform actual Proxmox operations."
|
||||
22
scripts/install/debian-install.sh
Executable file
22
scripts/install/debian-install.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2025 tteck
|
||||
# Author: tteck (tteckster)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://www.debian.org/
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
|
||||
msg_info "Cleaning up"
|
||||
$STD apt-get -y autoremove
|
||||
$STD apt-get -y autoclean
|
||||
msg_ok "Cleaned"
|
||||
14
scripts/test-script.sh
Executable file
14
scripts/test-script.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Hello from test script!"
|
||||
echo "Current directory: $(pwd)"
|
||||
echo "Script arguments: $@"
|
||||
echo "Environment variables:"
|
||||
env | grep -E "(PATH|HOME|USER)" | head -5
|
||||
|
||||
for i in {1..5}; do
|
||||
echo "Count: $i"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Test script completed!"
|
||||
101
server.js
101
server.js
@@ -4,12 +4,13 @@ import next from 'next';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { spawn } from 'child_process';
|
||||
import { join, resolve } from 'path';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { spawn as ptySpawn } from 'node-pty';
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
const hostname = 'localhost';
|
||||
const hostname = '0.0.0.0';
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
// when using middleware `hostname` and `port` must be provided below
|
||||
const app = next({ dev, hostname, port });
|
||||
const handle = app.getRequestHandler();
|
||||
|
||||
@@ -27,10 +28,19 @@ class ScriptExecutionHandler {
|
||||
setupWebSocket() {
|
||||
this.wss.on('connection', (ws, request) => {
|
||||
console.log('New WebSocket connection for script execution');
|
||||
console.log('Client IP:', request.socket.remoteAddress);
|
||||
console.log('User-Agent:', request.headers['user-agent']);
|
||||
console.log('WebSocket readyState:', ws.readyState);
|
||||
console.log('Request URL:', request.url);
|
||||
|
||||
// Set connection metadata
|
||||
ws.connectionTime = Date.now();
|
||||
ws.clientIP = request.socket.remoteAddress;
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
const message = JSON.parse(data.toString());
|
||||
console.log('Received message from client:', message);
|
||||
this.handleMessage(ws, message);
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
@@ -42,8 +52,8 @@ class ScriptExecutionHandler {
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('WebSocket connection closed');
|
||||
ws.on('close', (code, reason) => {
|
||||
console.log(`WebSocket connection closed: ${code} - ${reason}`);
|
||||
this.cleanupActiveExecutions(ws);
|
||||
});
|
||||
|
||||
@@ -55,13 +65,16 @@ class ScriptExecutionHandler {
|
||||
}
|
||||
|
||||
async handleMessage(ws, message) {
|
||||
const { action, scriptPath, executionId } = message;
|
||||
const { action, scriptPath, executionId, input } = message;
|
||||
console.log('Handling message:', { action, scriptPath, executionId });
|
||||
|
||||
switch (action) {
|
||||
case 'start':
|
||||
if (scriptPath && executionId) {
|
||||
console.log('Starting script execution for:', scriptPath);
|
||||
await this.startScriptExecution(ws, scriptPath, executionId);
|
||||
} else {
|
||||
console.log('Missing scriptPath or executionId');
|
||||
this.sendMessage(ws, {
|
||||
type: 'error',
|
||||
data: 'Missing scriptPath or executionId',
|
||||
@@ -76,6 +89,12 @@ class ScriptExecutionHandler {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'input':
|
||||
if (executionId && input !== undefined) {
|
||||
this.sendInputToProcess(executionId, input);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
this.sendMessage(ws, {
|
||||
type: 'error',
|
||||
@@ -87,11 +106,17 @@ class ScriptExecutionHandler {
|
||||
|
||||
async startScriptExecution(ws, scriptPath, executionId) {
|
||||
try {
|
||||
console.log('Starting script execution...');
|
||||
// Basic validation
|
||||
const scriptsDir = join(process.cwd(), 'scripts');
|
||||
const resolvedPath = resolve(scriptPath);
|
||||
|
||||
console.log('Scripts directory:', scriptsDir);
|
||||
console.log('Resolved path:', resolvedPath);
|
||||
console.log('Is within scripts dir:', resolvedPath.startsWith(resolve(scriptsDir)));
|
||||
|
||||
if (!resolvedPath.startsWith(resolve(scriptsDir))) {
|
||||
console.log('Script path validation failed');
|
||||
this.sendMessage(ws, {
|
||||
type: 'error',
|
||||
data: 'Script path is not within the allowed scripts directory',
|
||||
@@ -110,15 +135,25 @@ class ScriptExecutionHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start script execution
|
||||
const process = spawn('bash', [scriptPath], {
|
||||
// Start script execution with pty for proper TTY support
|
||||
const childProcess = ptySpawn('bash', [scriptPath], {
|
||||
cwd: scriptsDir,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
shell: true
|
||||
name: 'xterm-256color',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
env: {
|
||||
...process.env,
|
||||
TERM: 'xterm-256color', // Enable proper terminal support
|
||||
FORCE_ANSI: 'true', // Allow ANSI codes for proper display
|
||||
COLUMNS: '80', // Set terminal width
|
||||
LINES: '24' // Set terminal height
|
||||
}
|
||||
});
|
||||
|
||||
// pty handles encoding automatically
|
||||
|
||||
// Store the execution
|
||||
this.activeExecutions.set(executionId, { process, ws });
|
||||
this.activeExecutions.set(executionId, { process: childProcess, ws });
|
||||
|
||||
// Send start message
|
||||
this.sendMessage(ws, {
|
||||
@@ -127,8 +162,8 @@ class ScriptExecutionHandler {
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Handle stdout
|
||||
process.stdout?.on('data', (data) => {
|
||||
// Handle pty data (both stdout and stderr combined)
|
||||
childProcess.onData((data) => {
|
||||
this.sendMessage(ws, {
|
||||
type: 'output',
|
||||
data: data.toString(),
|
||||
@@ -136,32 +171,11 @@ class ScriptExecutionHandler {
|
||||
});
|
||||
});
|
||||
|
||||
// Handle stderr
|
||||
process.stderr?.on('data', (data) => {
|
||||
this.sendMessage(ws, {
|
||||
type: 'error',
|
||||
data: data.toString(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
});
|
||||
|
||||
// Handle process exit
|
||||
process.on('exit', (code, signal) => {
|
||||
childProcess.onExit((exitCode, signal) => {
|
||||
this.sendMessage(ws, {
|
||||
type: 'end',
|
||||
data: `Script execution finished with code: ${code}, signal: ${signal}`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Clean up
|
||||
this.activeExecutions.delete(executionId);
|
||||
});
|
||||
|
||||
// Handle process error
|
||||
process.on('error', (error) => {
|
||||
this.sendMessage(ws, {
|
||||
type: 'error',
|
||||
data: `Process error: ${error.message}`,
|
||||
data: `Script execution finished with code: ${exitCode}, signal: ${signal}`,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
@@ -192,6 +206,16 @@ class ScriptExecutionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
sendInputToProcess(executionId, input) {
|
||||
const execution = this.activeExecutions.get(executionId);
|
||||
if (execution && execution.process.write) {
|
||||
console.log('Sending input to process:', JSON.stringify(input), 'Length:', input.length);
|
||||
execution.process.write(input);
|
||||
} else {
|
||||
console.log('No active execution found for input:', executionId);
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(ws, message) {
|
||||
if (ws.readyState === 1) { // WebSocket.OPEN
|
||||
ws.send(JSON.stringify(message));
|
||||
@@ -208,6 +232,8 @@ class ScriptExecutionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// TerminalHandler removed - not used by current application
|
||||
|
||||
app.prepare().then(() => {
|
||||
const httpServer = createServer(async (req, res) => {
|
||||
try {
|
||||
@@ -229,15 +255,16 @@ app.prepare().then(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Create WebSocket handler
|
||||
// Create WebSocket handlers
|
||||
const scriptHandler = new ScriptExecutionHandler(httpServer);
|
||||
// Note: TerminalHandler removed as it's not being used by the current application
|
||||
|
||||
httpServer
|
||||
.once('error', (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
})
|
||||
.listen(port, () => {
|
||||
.listen(port, hostname, () => {
|
||||
console.log(`> Ready on http://${hostname}:${port}`);
|
||||
console.log(`> WebSocket server running on ws://${hostname}:${port}/ws/script-execution`);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { api } from '~/trpc/react';
|
||||
import { useState } from 'react';
|
||||
import { Terminal } from './Terminal';
|
||||
|
||||
|
||||
interface ScriptsListProps {
|
||||
onRunScript: (scriptPath: string, scriptName: string) => void;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { api } from '~/trpc/react';
|
||||
import { Terminal as XTerm } from '@xterm/xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
|
||||
interface TerminalProps {
|
||||
scriptPath: string;
|
||||
@@ -17,70 +20,203 @@ interface TerminalMessage {
|
||||
export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [output, setOutput] = useState<string[]>([]);
|
||||
const [executionId] = useState(() => `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`);
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
const xtermRef = useRef<XTerm | null>(null);
|
||||
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const outputRef = useRef<HTMLDivElement>(null);
|
||||
const [executionId] = useState(() => `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`);
|
||||
const isConnectingRef = useRef<boolean>(false);
|
||||
const hasConnectedRef = useRef<boolean>(false);
|
||||
|
||||
const scriptName = scriptPath.split('/').pop() || scriptPath.split('\\').pop() || 'Unknown Script';
|
||||
|
||||
useEffect(() => {
|
||||
// Connect to WebSocket
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/script-execution`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws;
|
||||
// Initialize xterm.js terminal with proper timing
|
||||
if (!terminalRef.current || xtermRef.current) return;
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
setIsConnected(true);
|
||||
// Use setTimeout to ensure DOM is fully ready
|
||||
const initTerminal = () => {
|
||||
if (!terminalRef.current || xtermRef.current) return;
|
||||
|
||||
const terminal = new XTerm({
|
||||
theme: {
|
||||
background: '#000000',
|
||||
foreground: '#00ff00',
|
||||
cursor: '#00ff00',
|
||||
},
|
||||
fontSize: 14,
|
||||
fontFamily: 'Courier New, monospace',
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'block',
|
||||
scrollback: 1000,
|
||||
tabStopWidth: 4,
|
||||
});
|
||||
|
||||
// Add addons
|
||||
const fitAddon = new FitAddon();
|
||||
const webLinksAddon = new WebLinksAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(webLinksAddon);
|
||||
|
||||
// Open terminal
|
||||
terminal.open(terminalRef.current);
|
||||
|
||||
// Fit after a small delay to ensure proper sizing
|
||||
setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
}, 100);
|
||||
|
||||
// Store references
|
||||
xtermRef.current = terminal;
|
||||
fitAddonRef.current = fitAddon;
|
||||
|
||||
// Handle terminal input
|
||||
terminal.onData((data) => {
|
||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.send(JSON.stringify({
|
||||
action: 'input',
|
||||
executionId,
|
||||
input: data
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle terminal resize
|
||||
const handleResize = () => {
|
||||
if (fitAddonRef.current) {
|
||||
fitAddonRef.current.fit();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
terminal.dispose();
|
||||
};
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const message: TerminalMessage = JSON.parse(event.data);
|
||||
handleMessage(message);
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
setIsConnected(false);
|
||||
setIsRunning(false);
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
setIsConnected(false);
|
||||
};
|
||||
// Initialize with a small delay
|
||||
const timeoutId = setTimeout(initTerminal, 50);
|
||||
|
||||
return () => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
clearTimeout(timeoutId);
|
||||
if (xtermRef.current) {
|
||||
xtermRef.current.dispose();
|
||||
xtermRef.current = null;
|
||||
fitAddonRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent multiple connections in React Strict Mode
|
||||
if (hasConnectedRef.current || isConnectingRef.current || (wsRef.current && wsRef.current.readyState === WebSocket.OPEN)) {
|
||||
console.log('WebSocket already connected, connecting, or has connected, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
// Close any existing connection first
|
||||
if (wsRef.current) {
|
||||
console.log('Closing existing WebSocket connection');
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
|
||||
isConnectingRef.current = true;
|
||||
hasConnectedRef.current = true;
|
||||
|
||||
// Small delay to prevent rapid reconnection
|
||||
const connectWithDelay = () => {
|
||||
// Connect to WebSocket
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/script-execution`;
|
||||
|
||||
console.log('Connecting to WebSocket:', wsUrl);
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected successfully');
|
||||
console.log('Script path:', scriptPath);
|
||||
console.log('Execution ID:', executionId);
|
||||
setIsConnected(true);
|
||||
isConnectingRef.current = false;
|
||||
|
||||
// Send start message immediately after connection
|
||||
ws.send(JSON.stringify({
|
||||
action: 'start',
|
||||
scriptPath,
|
||||
executionId
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const message: TerminalMessage = JSON.parse(event.data);
|
||||
console.log('Received message:', message);
|
||||
handleMessage(message);
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('WebSocket disconnected:', event.code, event.reason);
|
||||
setIsConnected(false);
|
||||
setIsRunning(false);
|
||||
isConnectingRef.current = false;
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
console.error('WebSocket readyState:', ws.readyState);
|
||||
setIsConnected(false);
|
||||
isConnectingRef.current = false;
|
||||
};
|
||||
};
|
||||
|
||||
// Add small delay to prevent rapid reconnection
|
||||
const timeoutId = setTimeout(connectWithDelay, 100);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
isConnectingRef.current = false;
|
||||
hasConnectedRef.current = false;
|
||||
if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) {
|
||||
console.log('Cleaning up WebSocket connection');
|
||||
wsRef.current.close();
|
||||
}
|
||||
};
|
||||
}, [scriptPath, executionId]);
|
||||
|
||||
const handleMessage = (message: TerminalMessage) => {
|
||||
if (!xtermRef.current) return;
|
||||
|
||||
const timestamp = new Date(message.timestamp).toLocaleTimeString();
|
||||
const prefix = `[${timestamp}] `;
|
||||
|
||||
switch (message.type) {
|
||||
case 'start':
|
||||
setOutput(prev => [...prev, `${prefix}🚀 ${message.data}`]);
|
||||
xtermRef.current.writeln(`${prefix}🚀 ${message.data}`);
|
||||
setIsRunning(true);
|
||||
break;
|
||||
case 'output':
|
||||
setOutput(prev => [...prev, message.data]);
|
||||
// Write directly to terminal - xterm.js handles ANSI codes natively
|
||||
xtermRef.current.write(message.data);
|
||||
break;
|
||||
case 'error':
|
||||
setOutput(prev => [...prev, `${prefix}❌ ${message.data}`]);
|
||||
// Check if this looks like ANSI terminal output (contains escape codes)
|
||||
if (message.data.includes('\x1B[') || message.data.includes('\u001b[')) {
|
||||
// This is likely terminal output sent to stderr, treat it as normal output
|
||||
xtermRef.current.write(message.data);
|
||||
} else {
|
||||
// This is a real error, show it with error prefix
|
||||
xtermRef.current.writeln(`${prefix}❌ ${message.data}`);
|
||||
}
|
||||
break;
|
||||
case 'end':
|
||||
setOutput(prev => [...prev, `${prefix}✅ ${message.data}`]);
|
||||
xtermRef.current.writeln(`${prefix}✅ ${message.data}`);
|
||||
setIsRunning(false);
|
||||
break;
|
||||
}
|
||||
@@ -106,15 +242,10 @@ export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
||||
};
|
||||
|
||||
const clearOutput = () => {
|
||||
setOutput([]);
|
||||
};
|
||||
|
||||
// Auto-scroll to bottom when new output arrives
|
||||
useEffect(() => {
|
||||
if (outputRef.current) {
|
||||
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
||||
if (xtermRef.current) {
|
||||
xtermRef.current.clear();
|
||||
}
|
||||
}, [output]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 rounded-lg border border-gray-700 overflow-hidden">
|
||||
@@ -141,23 +272,10 @@ export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
||||
|
||||
{/* Terminal Output */}
|
||||
<div
|
||||
ref={outputRef}
|
||||
className="h-96 overflow-y-auto p-4 font-mono text-sm"
|
||||
style={{ backgroundColor: '#000000', color: '#00ff00' }}
|
||||
>
|
||||
{output.length === 0 ? (
|
||||
<div className="text-gray-500">
|
||||
<p>Terminal ready. Click "Start Script" to begin execution.</p>
|
||||
<p className="mt-2">Script: {scriptPath}</p>
|
||||
</div>
|
||||
) : (
|
||||
output.map((line, index) => (
|
||||
<div key={index} className="whitespace-pre-wrap">
|
||||
{line}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
ref={terminalRef}
|
||||
className="h-96 w-full"
|
||||
style={{ minHeight: '384px' }}
|
||||
/>
|
||||
|
||||
{/* Terminal Controls */}
|
||||
<div className="bg-gray-800 px-4 py-2 flex items-center justify-between border-t border-gray-700">
|
||||
@@ -203,4 +321,4 @@ export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -69,8 +69,11 @@ export class ScriptManager {
|
||||
*/
|
||||
private async isExecutable(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await access(filePath, 0o111); // Check execute permission
|
||||
return true;
|
||||
const stats = await stat(filePath);
|
||||
// Check if file has execute permission for owner, group, or others
|
||||
const mode = stats.mode;
|
||||
const isExecutable = !!(mode & parseInt('111', 8));
|
||||
return isExecutable;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4,3 +4,19 @@
|
||||
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
/* Terminal-specific styles for ANSI escape code rendering */
|
||||
.terminal-output {
|
||||
font-family: 'Courier New', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.terminal-output span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* Ensure proper rendering of ANSI colors */
|
||||
.terminal-output * {
|
||||
color: inherit;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user