From f8a2370336d764d00acdadb767104cc0a1949dc0 Mon Sep 17 00:00:00 2001 From: Master3395 Date: Wed, 17 Dec 2025 19:48:28 +0100 Subject: [PATCH] Implement dynamic PHP version detection and configuration updates - Added a new function `detect_default_php` to dynamically determine the default PHP version based on symlink and available versions, enhancing compatibility with PHP 7.4-8.5. - Updated `phpmyadmin_limits` to utilize the detected PHP version for configuration changes, ensuring accurate parameter adjustments for the current PHP environment. - Enhanced installation scripts for Redis and Memcached extensions to support PHP versions 7.4-8.5, improving backward compatibility and installation reliability. - Improved error handling for missing PHP configuration files, providing clearer feedback to users. These changes enhance the flexibility and robustness of PHP management within CyberPanel. --- cyberpanel_utility.sh | 120 +++++++++++++++++---- install/install.py | 232 +++++++++++++++++++++++++++++++++++++++-- plogical/upgrade.py | 236 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 556 insertions(+), 32 deletions(-) diff --git a/cyberpanel_utility.sh b/cyberpanel_utility.sh index 31d1607f9..26c89743a 100644 --- a/cyberpanel_utility.sh +++ b/cyberpanel_utility.sh @@ -175,6 +175,52 @@ curl --silent https://cyberpanel.sh/misc/faq.sh | sudo -u nobody bash | less -r exit } +detect_default_php() { + # Detect default PHP version dynamically + # Priority: Check symlink, then find highest available version + + local php_version="" + local php_version_formatted="" + + # Method 1: Check default PHP symlink + if [[ -L /usr/local/lscp/fcgi-bin/lsphp ]]; then + local default_php_path=$(readlink -f /usr/local/lscp/fcgi-bin/lsphp 2>/dev/null) + if [[ -n "$default_php_path" ]]; then + # Extract version from path like /usr/local/lsws/lsphp82/bin/lsphp + # Use sed for better portability (works on all systems) + php_version=$(echo "$default_php_path" | sed -n 's|.*/lsphp\([0-9][0-9]\)/.*|\1|p') + if [[ -n "$php_version" ]] && [[ ${#php_version} -ge 2 ]]; then + php_version_formatted="${php_version:0:1}.${php_version:1}" + fi + fi + fi + + # Method 2: Find highest available PHP version (fallback) + if [[ -z "$php_version" ]]; then + # Priority: 85, 84, 83, 82, 81, 80, 74, 73, 72 (newest to oldest, supporting 7.4-8.5) + local php_versions=('85' '84' '83' '82' '81' '80' '74' '73' '72') + for ver in "${php_versions[@]}"; do + if [[ -d "/usr/local/lsws/lsphp${ver}" ]] && [[ -f "/usr/local/lsws/lsphp${ver}/bin/lsphp" ]]; then + php_version="$ver" + if [[ ${#ver} -ge 2 ]]; then + php_version_formatted="${ver:0:1}.${ver:1}" + fi + break + fi + done + fi + + # Fallback to PHP 7.4 if nothing found (backwards compatibility) + if [[ -z "$php_version" ]]; then + if [[ -d "/usr/local/lsws/lsphp74" ]]; then + php_version="74" + php_version_formatted="7.4" + fi + fi + + echo "$php_version|$php_version_formatted" +} + addons() { echo -e "\nPlease choose:" echo -e "\n1. Install Memcached extension for PHP." @@ -205,29 +251,49 @@ addons() { } phpmyadmin_limits() { - echo -e "This will change following parameters for PHP 7.3:" + # Detect default PHP version dynamically + local php_info=$(detect_default_php) + local php_version=$(echo "$php_info" | cut -d'|' -f1) + local php_version_formatted=$(echo "$php_info" | cut -d'|' -f2) + + if [[ -z "$php_version" ]] || [[ -z "$php_version_formatted" ]]; then + echo -e "\nError: Could not detect PHP version. Please ensure PHP is installed." + exit 1 + fi + + echo -e "This will change following parameters for PHP ${php_version_formatted}:" echo -e "Post Max Size from default 8M to 500M" echo -e "Upload Max Filesize from default 2M to 500M" echo -e "Memory Limit from default 128M to 768M" echo -e "Max Execution Time from default 30 to 600" - echo -e "\nPlease note this will also apply to all sites use PHP 7.3" + echo -e "\nPlease note this will also apply to all sites using PHP ${php_version_formatted}" printf "%s" "Please confirm to proceed: [Y/n]: " read TMP_YN if [[ $TMP_YN == "Y" ]] || [[ $TMP_YN == "y" ]] ; then + local php_ini_path="" + + # Determine php.ini path based on OS and PHP version if [[ "$SERVER_OS" == "CentOS" ]] || [[ "$SERVER_OS" == "openEuler" ]] ; then - php_ini_path="/usr/local/lsws/lsphp73/etc/php.ini" - fi - - if [[ "$SERVER_OS" == "Ubuntu" ]] ; then - php_ini_path="/usr/local/lsws/lsphp73/etc/php/7.3/litespeed/php.ini" - fi - sed -i 's|post_max_size = 8M|post_max_size = 500M|g' $php_ini_path - sed -i 's|upload_max_filesize = 2M|upload_max_filesize = 500M |g' $php_ini_path - sed -i 's|memory_limit = 128M|memory_limit = 768M|g' $php_ini_path - sed -i 's|max_execution_time = 30|max_execution_time = 600|g' $php_ini_path - systemctl restart lscpd - echo "Change applied..." + php_ini_path="/usr/local/lsws/lsphp${php_version}/etc/php.ini" + elif [[ "$SERVER_OS" == "Ubuntu" ]] ; then + php_ini_path="/usr/local/lsws/lsphp${php_version}/etc/php/${php_version_formatted}/litespeed/php.ini" + fi + + # Verify php.ini file exists + if [[ ! -f "$php_ini_path" ]]; then + echo -e "\nError: PHP configuration file not found at: $php_ini_path" + echo -e "Please verify PHP ${php_version_formatted} is properly installed." + exit 1 + fi + + # Apply changes + sed -i 's|post_max_size = 8M|post_max_size = 500M|g' "$php_ini_path" + sed -i 's|upload_max_filesize = 2M|upload_max_filesize = 500M |g' "$php_ini_path" + sed -i 's|memory_limit = 128M|memory_limit = 768M|g' "$php_ini_path" + sed -i 's|max_execution_time = 30|max_execution_time = 600|g' "$php_ini_path" + systemctl restart lscpd + echo "Change applied to PHP ${php_version_formatted} configuration..." else echo -e "Please enter Y or n." exit @@ -235,14 +301,21 @@ phpmyadmin_limits() { } install_php_redis() { + # Install Redis extension for PHP 7.4-8.5 (backwards compatible) if [[ $SERVER_OS == "CentOS" ]] ; then - yum install -y lsphp74-redis lsphp73-redis lsphp72-redis lsphp71-redis lsphp70-redis lsphp56-redis lsphp55-redis lsphp54-redis + yum install -y lsphp85-redis lsphp84-redis lsphp83-redis lsphp82-redis lsphp81-redis lsphp80-redis lsphp74-redis lsphp73-redis lsphp72-redis lsphp71-redis lsphp70-redis lsphp56-redis lsphp55-redis lsphp54-redis 2>/dev/null || \ + yum install -y lsphp84-redis lsphp83-redis lsphp82-redis lsphp81-redis lsphp80-redis lsphp74-redis lsphp73-redis lsphp72-redis lsphp71-redis lsphp70-redis lsphp56-redis lsphp55-redis lsphp54-redis 2>/dev/null || \ + yum install -y lsphp83-redis lsphp82-redis lsphp81-redis lsphp80-redis lsphp74-redis lsphp73-redis lsphp72-redis lsphp71-redis lsphp70-redis lsphp56-redis lsphp55-redis lsphp54-redis fi if [[ $SERVER_OS == "Ubuntu" ]] ; then - DEBIAN_FRONTEND=noninteractive apt install -y lsphp74-redis lsphp73-redis lsphp72-redis lsphp71-redis lsphp70-redis + DEBIAN_FRONTEND=noninteractive apt install -y lsphp85-redis lsphp84-redis lsphp83-redis lsphp82-redis lsphp81-redis lsphp80-redis lsphp74-redis lsphp73-redis lsphp72-redis lsphp71-redis lsphp70-redis 2>/dev/null || \ + DEBIAN_FRONTEND=noninteractive apt install -y lsphp84-redis lsphp83-redis lsphp82-redis lsphp81-redis lsphp80-redis lsphp74-redis lsphp73-redis lsphp72-redis lsphp71-redis lsphp70-redis 2>/dev/null || \ + DEBIAN_FRONTEND=noninteractive apt install -y lsphp83-redis lsphp82-redis lsphp81-redis lsphp80-redis lsphp74-redis lsphp73-redis lsphp72-redis lsphp71-redis lsphp70-redis fi if [[ $SERVER_OS == "openEuler" ]] ; then - dnf install -y lsphp74-redis lsphp73-redis lsphp72-redis lsphp71-redis + dnf install -y lsphp85-redis lsphp84-redis lsphp83-redis lsphp82-redis lsphp81-redis lsphp80-redis lsphp74-redis lsphp73-redis lsphp72-redis lsphp71-redis 2>/dev/null || \ + dnf install -y lsphp84-redis lsphp83-redis lsphp82-redis lsphp81-redis lsphp80-redis lsphp74-redis lsphp73-redis lsphp72-redis lsphp71-redis 2>/dev/null || \ + dnf install -y lsphp83-redis lsphp82-redis lsphp81-redis lsphp80-redis lsphp74-redis lsphp73-redis lsphp72-redis lsphp71-redis fi echo -e "\nRedis extension for PHP has been installed..." exit @@ -359,14 +432,21 @@ read TMP_YN } install_php_memcached() { + # Install Memcached extension for PHP 7.4-8.5 (backwards compatible) if [[ $SERVER_OS == "CentOS" ]] ; then - yum install -y lsphp74-memcached lsphp73-memcached lsphp72-memcached lsphp71-memcached lsphp70-memcached lsphp56-pecl-memcached lsphp55-pecl-memcached lsphp54-pecl-memcached + yum install -y lsphp85-memcached lsphp84-memcached lsphp83-memcached lsphp82-memcached lsphp81-memcached lsphp80-memcached lsphp74-memcached lsphp73-memcached lsphp72-memcached lsphp71-memcached lsphp70-memcached lsphp56-pecl-memcached lsphp55-pecl-memcached lsphp54-pecl-memcached 2>/dev/null || \ + yum install -y lsphp84-memcached lsphp83-memcached lsphp82-memcached lsphp81-memcached lsphp80-memcached lsphp74-memcached lsphp73-memcached lsphp72-memcached lsphp71-memcached lsphp70-memcached lsphp56-pecl-memcached lsphp55-pecl-memcached lsphp54-pecl-memcached 2>/dev/null || \ + yum install -y lsphp83-memcached lsphp82-memcached lsphp81-memcached lsphp80-memcached lsphp74-memcached lsphp73-memcached lsphp72-memcached lsphp71-memcached lsphp70-memcached lsphp56-pecl-memcached lsphp55-pecl-memcached lsphp54-pecl-memcached fi if [[ $SERVER_OS == "Ubuntu" ]] ; then - DEBIAN_FRONTEND=noninteractive apt install -y lsphp74-memcached lsphp73-memcached lsphp72-memcached lsphp71-memcached lsphp70-memcached + DEBIAN_FRONTEND=noninteractive apt install -y lsphp85-memcached lsphp84-memcached lsphp83-memcached lsphp82-memcached lsphp81-memcached lsphp80-memcached lsphp74-memcached lsphp73-memcached lsphp72-memcached lsphp71-memcached lsphp70-memcached 2>/dev/null || \ + DEBIAN_FRONTEND=noninteractive apt install -y lsphp84-memcached lsphp83-memcached lsphp82-memcached lsphp81-memcached lsphp80-memcached lsphp74-memcached lsphp73-memcached lsphp72-memcached lsphp71-memcached lsphp70-memcached 2>/dev/null || \ + DEBIAN_FRONTEND=noninteractive apt install -y lsphp83-memcached lsphp82-memcached lsphp81-memcached lsphp80-memcached lsphp74-memcached lsphp73-memcached lsphp72-memcached lsphp71-memcached lsphp70-memcached fi if [[ $SERVER_OS == "openEuler" ]] ; then - dnf install -y lsphp74-memcached lsphp73-memcached lsphp72-memcached lsphp71-memcached + dnf install -y lsphp85-memcached lsphp84-memcached lsphp83-memcached lsphp82-memcached lsphp81-memcached lsphp80-memcached lsphp74-memcached lsphp73-memcached lsphp72-memcached lsphp71-memcached 2>/dev/null || \ + dnf install -y lsphp84-memcached lsphp83-memcached lsphp82-memcached lsphp81-memcached lsphp80-memcached lsphp74-memcached lsphp73-memcached lsphp72-memcached lsphp71-memcached 2>/dev/null || \ + dnf install -y lsphp83-memcached lsphp82-memcached lsphp81-memcached lsphp80-memcached lsphp74-memcached lsphp73-memcached lsphp72-memcached lsphp71-memcached fi echo -e "\nMemcached extension for PHP has been installed..." exit diff --git a/install/install.py b/install/install.py index b94cb671b..b257dddcd 100644 --- a/install/install.py +++ b/install/install.py @@ -829,13 +829,196 @@ class preFlightsChecks: if any(distro in content for distro in ['red hat', 'almalinux', 'rocky', 'cloudlinux', 'centos']): return 'rhel9' - # Default to rhel9 if can't detect (safer default for newer systems) - self.stdOut("WARNING: Could not detect platform, defaulting to rhel9", 1) - return 'rhel9' + # Default to rhel8 if can't detect (safer default - rhel9 binaries may require GLIBC 2.35) + self.stdOut("WARNING: Could not detect platform, defaulting to rhel8", 1) + return 'rhel8' except Exception as msg: - self.stdOut(f"ERROR detecting platform: {msg}, defaulting to rhel9", 0) - return 'rhel9' + self.stdOut(f"ERROR detecting platform: {msg}, defaulting to rhel8", 0) + return 'rhel8' + + def getSystemGLIBCVersion(self): + """Get the system's GLIBC version""" + try: + import subprocess + # Try to get GLIBC version from ldd + result = subprocess.run(['ldd', '--version'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + # ldd --version output format: "ldd (GNU libc) 2.34" + for line in result.stdout.split('\n'): + if 'GNU libc' in line or 'glibc' in line.lower(): + import re + version_match = re.search(r'(\d+)\.(\d+)', line) + if version_match: + major = int(version_match.group(1)) + minor = int(version_match.group(2)) + return (major, minor) + + # Fallback: try to read from libc.so.6 + try: + result = subprocess.run(['/lib64/libc.so.6'], capture_output=True, text=True, timeout=5) + if result.returncode != 0 and 'version' in result.stderr.lower(): + import re + version_match = re.search(r'(\d+)\.(\d+)', result.stderr) + if version_match: + major = int(version_match.group(1)) + minor = int(version_match.group(2)) + return (major, minor) + except: + pass + + # If we can't detect, assume a safe minimum + self.stdOut("WARNING: Could not detect GLIBC version, assuming 2.34", 1) + return (2, 34) + except Exception as msg: + self.stdOut(f"WARNING: Error detecting GLIBC version: {msg}, assuming 2.34", 0) + return (2, 34) + + def checkBinaryGLIBCRequirements(self, binary_path): + """Check GLIBC version requirements of a binary file""" + try: + import subprocess + import re + + # Use objdump to check GLIBC version requirements + # objdump -T shows dynamic symbols and their GLIBC version requirements + result = subprocess.run(['objdump', '-T', binary_path], capture_output=True, text=True, timeout=10) + + if result.returncode != 0: + # objdump might not be available, try readelf + result = subprocess.run(['readelf', '-d', binary_path], capture_output=True, text=True, timeout=10) + if result.returncode != 0: + self.stdOut("WARNING: Could not check binary GLIBC requirements (objdump/readelf not available)", 1) + return None + + # Look for GLIBC version requirements in the output + # Format: GLIBC_2.35, GLIBC_2.34, etc. + max_version = None + for line in result.stdout.split('\n') + result.stderr.split('\n'): + # Look for GLIBC version symbols + matches = re.findall(r'GLIBC_(\d+)\.(\d+)', line) + for match in matches: + major = int(match[0]) + minor = int(match[1]) + if max_version is None or (major, minor) > max_version: + max_version = (major, minor) + + return max_version + + except FileNotFoundError: + # objdump/readelf not available + self.stdOut("WARNING: objdump/readelf not available, skipping GLIBC check", 1) + return None + except Exception as msg: + self.stdOut(f"WARNING: Error checking binary GLIBC requirements: {msg}", 0) + return None + + def verifyBinaryCompatibility(self, binary_path): + """Verify that a binary is compatible with the system's GLIBC version""" + try: + system_glibc = self.getSystemGLIBCVersion() + binary_glibc = self.checkBinaryGLIBCRequirements(binary_path) + + if binary_glibc is None: + # Can't check, but we can try a test run + self.stdOut("Cannot verify GLIBC requirements, performing test run...", 1) + return self.testBinaryExecution(binary_path) + + self.stdOut(f"System GLIBC: {system_glibc[0]}.{system_glibc[1]}", 1) + self.stdOut(f"Binary requires GLIBC: {binary_glibc[0]}.{binary_glibc[1]}", 1) + + # Check if binary requires newer GLIBC than system has + if binary_glibc > system_glibc: + self.stdOut(f"ERROR: Binary requires GLIBC {binary_glibc[0]}.{binary_glibc[1]}, but system has {system_glibc[0]}.{system_glibc[1]}", 1) + return False + + self.stdOut("GLIBC compatibility check passed", 1) + return True + + except Exception as msg: + self.stdOut(f"WARNING: Error verifying binary compatibility: {msg}", 0) + # If we can't verify, try test execution + return self.testBinaryExecution(binary_path) + + def testBinaryExecution(self, binary_path): + """Test if binary can execute (checks GLIBC compatibility indirectly)""" + try: + import subprocess + # Try to run the binary with --version or -v flag + # This will fail immediately if GLIBC is incompatible + # The error format is: "./binary: /lib64/libc.so.6: version `GLIBC_X.Y' not found" + result = subprocess.run([binary_path, '--version'], capture_output=True, text=True, timeout=5) + + # Check both stdout and stderr for GLIBC errors + output = result.stdout + result.stderr + + # Look for GLIBC version not found errors + if 'GLIBC' in output and ('not found' in output or 'version' in output.lower()): + # Extract the required GLIBC version from error message + import re + glibc_match = re.search(r"GLIBC_(\d+)\.(\d+)'?\s+not found", output) + if glibc_match: + required_major = int(glibc_match.group(1)) + required_minor = int(glibc_match.group(2)) + self.stdOut(f"ERROR: Binary requires GLIBC {required_major}.{required_minor} which is not available", 1) + else: + self.stdOut(f"ERROR: Binary GLIBC compatibility test failed: {output[:200]}", 1) + return False + + # If binary executed (even with non-zero return code for --version), it's compatible + if result.returncode == 0 or len(result.stdout) > 0: + return True + + # If we get here, binary might not support --version, try -v + result = subprocess.run([binary_path, '-v'], capture_output=True, text=True, timeout=5) + output = result.stdout + result.stderr + + if 'GLIBC' in output and ('not found' in output or 'version' in output.lower()): + import re + glibc_match = re.search(r"GLIBC_(\d+)\.(\d+)'?\s+not found", output) + if glibc_match: + required_major = int(glibc_match.group(1)) + required_minor = int(glibc_match.group(2)) + self.stdOut(f"ERROR: Binary requires GLIBC {required_major}.{required_minor} which is not available", 1) + else: + self.stdOut(f"ERROR: Binary GLIBC compatibility test failed: {output[:200]}", 1) + return False + + # If no GLIBC error and we got some output, assume compatible + if len(output) > 0: + return True + + # If binary doesn't support --version/-v, try to check if it's executable + # by checking file type + try: + result = subprocess.run(['file', binary_path], capture_output=True, text=True, timeout=5) + if 'ELF' in result.stdout and 'executable' in result.stdout: + self.stdOut("WARNING: Cannot test binary execution, but file appears valid", 1) + return True + except: + pass + + # Conservative approach: if we can't verify, assume incompatible to be safe + self.stdOut("WARNING: Could not verify binary execution, skipping installation for safety", 1) + return False + + except subprocess.TimeoutExpired: + self.stdOut("WARNING: Binary test timed out, assuming compatible", 1) + return True + except FileNotFoundError as e: + # Binary file not found or command not found + self.stdOut(f"ERROR: Binary test failed - file or command not found: {e}", 1) + return False + except Exception as msg: + # Check if it's a GLIBC error in the exception itself + error_str = str(msg) + if 'GLIBC' in error_str and 'not found' in error_str: + self.stdOut(f"ERROR: Binary GLIBC compatibility test failed: {error_str}", 1) + return False + + self.stdOut(f"WARNING: Could not test binary execution: {msg}", 0) + # Conservative approach: if we can't test, assume incompatible to be safe + return False def downloadCustomBinary(self, url, destination, expected_sha256=None): """Download custom binary file with optional checksum verification""" @@ -964,6 +1147,23 @@ class preFlightsChecks: self.stdOut("Continuing with standard OLS", 1) return True # Not fatal, continue with standard OLS + # CRITICAL: Verify GLIBC compatibility before installation + self.stdOut("Verifying GLIBC compatibility...", 1) + if not self.verifyBinaryCompatibility(tmp_binary): + self.stdOut("=" * 50, 1) + self.stdOut("ERROR: Binary GLIBC requirements incompatible with system", 1) + self.stdOut("This binary would cause OpenLiteSpeed to fail to start", 1) + self.stdOut("Skipping custom binary installation to preserve system stability", 1) + self.stdOut("Standard OLS binary from package manager will be used", 1) + self.stdOut("=" * 50, 1) + # Clean up downloaded binary + try: + if os.path.exists(tmp_binary): + os.remove(tmp_binary) + except: + pass + return True # Not fatal, continue with standard OLS + # Download module with checksum verification (if available) module_downloaded = False if MODULE_URL and MODULE_SHA256: @@ -979,11 +1179,31 @@ class preFlightsChecks: self.stdOut("Installing custom binaries...", 1) try: + # Make binary executable before moving + os.chmod(tmp_binary, 0o755) + + # Final compatibility test before installation + if not self.testBinaryExecution(tmp_binary): + self.stdOut("ERROR: Final binary compatibility test failed", 1) + self.stdOut("Skipping installation to prevent OpenLiteSpeed failure", 1) + try: + if os.path.exists(tmp_binary): + os.remove(tmp_binary) + except: + pass + return True # Not fatal, continue with standard OLS + shutil.move(tmp_binary, OLS_BINARY_PATH) - os.chmod(OLS_BINARY_PATH, 0o755) self.stdOut("Installed OpenLiteSpeed binary", 1) except Exception as e: self.stdOut(f"ERROR: Failed to install binary: {e}", 1) + # Try to restore backup if installation failed + try: + if os.path.exists(f"{backup_dir}/openlitespeed.backup"): + shutil.copy2(f"{backup_dir}/openlitespeed.backup", OLS_BINARY_PATH) + self.stdOut("Restored original binary from backup", 1) + except: + pass return False # Install module (if downloaded) diff --git a/plogical/upgrade.py b/plogical/upgrade.py index 130031bd4..bd7d19a6c 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -674,13 +674,200 @@ class Upgrade: if any(distro in content for distro in ['red hat', 'almalinux', 'rocky', 'cloudlinux', 'centos']): return 'rhel9' - # Default to rhel9 if can't detect (safer default for newer systems) - Upgrade.stdOut("WARNING: Could not detect platform, defaulting to rhel9", 0) - return 'rhel9' + # Default to rhel8 if can't detect (safer default - rhel9 binaries may require GLIBC 2.35) + Upgrade.stdOut("WARNING: Could not detect platform, defaulting to rhel8", 0) + return 'rhel8' except Exception as msg: - Upgrade.stdOut(f"ERROR detecting platform: {msg}, defaulting to rhel9", 0) - return 'rhel9' + Upgrade.stdOut(f"ERROR detecting platform: {msg}, defaulting to rhel8", 0) + return 'rhel8' + + @staticmethod + def getSystemGLIBCVersion(): + """Get the system's GLIBC version""" + try: + import subprocess + # Try to get GLIBC version from ldd + result = subprocess.run(['ldd', '--version'], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + # ldd --version output format: "ldd (GNU libc) 2.34" + for line in result.stdout.split('\n'): + if 'GNU libc' in line or 'glibc' in line.lower(): + import re + version_match = re.search(r'(\d+)\.(\d+)', line) + if version_match: + major = int(version_match.group(1)) + minor = int(version_match.group(2)) + return (major, minor) + + # Fallback: try to read from libc.so.6 + try: + result = subprocess.run(['/lib64/libc.so.6'], capture_output=True, text=True, timeout=5) + if result.returncode != 0 and 'version' in result.stderr.lower(): + import re + version_match = re.search(r'(\d+)\.(\d+)', result.stderr) + if version_match: + major = int(version_match.group(1)) + minor = int(version_match.group(2)) + return (major, minor) + except: + pass + + # If we can't detect, assume a safe minimum + Upgrade.stdOut("WARNING: Could not detect GLIBC version, assuming 2.34", 0) + return (2, 34) + except Exception as msg: + Upgrade.stdOut(f"WARNING: Error detecting GLIBC version: {msg}, assuming 2.34", 0) + return (2, 34) + + @staticmethod + def checkBinaryGLIBCRequirements(binary_path): + """Check GLIBC version requirements of a binary file""" + try: + import subprocess + import re + + # Use objdump to check GLIBC version requirements + # objdump -T shows dynamic symbols and their GLIBC version requirements + result = subprocess.run(['objdump', '-T', binary_path], capture_output=True, text=True, timeout=10) + + if result.returncode != 0: + # objdump might not be available, try readelf + result = subprocess.run(['readelf', '-d', binary_path], capture_output=True, text=True, timeout=10) + if result.returncode != 0: + Upgrade.stdOut("WARNING: Could not check binary GLIBC requirements (objdump/readelf not available)", 0) + return None + + # Look for GLIBC version requirements in the output + # Format: GLIBC_2.35, GLIBC_2.34, etc. + max_version = None + for line in result.stdout.split('\n') + result.stderr.split('\n'): + # Look for GLIBC version symbols + matches = re.findall(r'GLIBC_(\d+)\.(\d+)', line) + for match in matches: + major = int(match[0]) + minor = int(match[1]) + if max_version is None or (major, minor) > max_version: + max_version = (major, minor) + + return max_version + + except FileNotFoundError: + # objdump/readelf not available + Upgrade.stdOut("WARNING: objdump/readelf not available, skipping GLIBC check", 0) + return None + except Exception as msg: + Upgrade.stdOut(f"WARNING: Error checking binary GLIBC requirements: {msg}", 0) + return None + + @staticmethod + def verifyBinaryCompatibility(binary_path): + """Verify that a binary is compatible with the system's GLIBC version""" + try: + system_glibc = Upgrade.getSystemGLIBCVersion() + binary_glibc = Upgrade.checkBinaryGLIBCRequirements(binary_path) + + if binary_glibc is None: + # Can't check, but we can try a test run + Upgrade.stdOut("Cannot verify GLIBC requirements, performing test run...", 0) + return Upgrade.testBinaryExecution(binary_path) + + Upgrade.stdOut(f"System GLIBC: {system_glibc[0]}.{system_glibc[1]}", 0) + Upgrade.stdOut(f"Binary requires GLIBC: {binary_glibc[0]}.{binary_glibc[1]}", 0) + + # Check if binary requires newer GLIBC than system has + if binary_glibc > system_glibc: + Upgrade.stdOut(f"ERROR: Binary requires GLIBC {binary_glibc[0]}.{binary_glibc[1]}, but system has {system_glibc[0]}.{system_glibc[1]}", 0) + return False + + Upgrade.stdOut("GLIBC compatibility check passed", 0) + return True + + except Exception as msg: + Upgrade.stdOut(f"WARNING: Error verifying binary compatibility: {msg}", 0) + # If we can't verify, try test execution + return Upgrade.testBinaryExecution(binary_path) + + @staticmethod + def testBinaryExecution(binary_path): + """Test if binary can execute (checks GLIBC compatibility indirectly)""" + try: + import subprocess + # Try to run the binary with --version or -v flag + # This will fail immediately if GLIBC is incompatible + # The error format is: "./binary: /lib64/libc.so.6: version `GLIBC_X.Y' not found" + result = subprocess.run([binary_path, '--version'], capture_output=True, text=True, timeout=5) + + # Check both stdout and stderr for GLIBC errors + output = result.stdout + result.stderr + + # Look for GLIBC version not found errors + if 'GLIBC' in output and ('not found' in output or 'version' in output.lower()): + # Extract the required GLIBC version from error message + import re + glibc_match = re.search(r"GLIBC_(\d+)\.(\d+)'?\s+not found", output) + if glibc_match: + required_major = int(glibc_match.group(1)) + required_minor = int(glibc_match.group(2)) + Upgrade.stdOut(f"ERROR: Binary requires GLIBC {required_major}.{required_minor} which is not available", 0) + else: + Upgrade.stdOut(f"ERROR: Binary GLIBC compatibility test failed: {output[:200]}", 0) + return False + + # If binary executed (even with non-zero return code for --version), it's compatible + if result.returncode == 0 or len(result.stdout) > 0: + return True + + # If we get here, binary might not support --version, try -v + result = subprocess.run([binary_path, '-v'], capture_output=True, text=True, timeout=5) + output = result.stdout + result.stderr + + if 'GLIBC' in output and ('not found' in output or 'version' in output.lower()): + import re + glibc_match = re.search(r"GLIBC_(\d+)\.(\d+)'?\s+not found", output) + if glibc_match: + required_major = int(glibc_match.group(1)) + required_minor = int(glibc_match.group(2)) + Upgrade.stdOut(f"ERROR: Binary requires GLIBC {required_major}.{required_minor} which is not available", 0) + else: + Upgrade.stdOut(f"ERROR: Binary GLIBC compatibility test failed: {output[:200]}", 0) + return False + + # If no GLIBC error and we got some output, assume compatible + if len(output) > 0: + return True + + # If binary doesn't support --version/-v, try to check if it's executable + # by checking file type + try: + result = subprocess.run(['file', binary_path], capture_output=True, text=True, timeout=5) + if 'ELF' in result.stdout and 'executable' in result.stdout: + Upgrade.stdOut("WARNING: Cannot test binary execution, but file appears valid", 0) + return True + except: + pass + + # Conservative approach: if we can't verify, assume incompatible to be safe + Upgrade.stdOut("WARNING: Could not verify binary execution, skipping installation for safety", 0) + return False + + except subprocess.TimeoutExpired: + Upgrade.stdOut("WARNING: Binary test timed out, assuming compatible", 0) + return True + except FileNotFoundError as e: + # Binary file not found or command not found + Upgrade.stdOut(f"ERROR: Binary test failed - file or command not found: {e}", 0) + return False + except Exception as msg: + # Check if it's a GLIBC error in the exception itself + error_str = str(msg) + if 'GLIBC' in error_str and 'not found' in error_str: + Upgrade.stdOut(f"ERROR: Binary GLIBC compatibility test failed: {error_str}", 0) + return False + + Upgrade.stdOut(f"WARNING: Could not test binary execution: {msg}", 0) + # Conservative approach: if we can't test, assume incompatible to be safe + return False @staticmethod def downloadCustomBinary(url, destination, expected_sha256=None): @@ -811,6 +998,23 @@ class Upgrade: Upgrade.stdOut("Continuing with standard OLS", 0) return True # Not fatal, continue with standard OLS + # CRITICAL: Verify GLIBC compatibility before installation + Upgrade.stdOut("Verifying GLIBC compatibility...", 0) + if not Upgrade.verifyBinaryCompatibility(tmp_binary): + Upgrade.stdOut("=" * 50, 0) + Upgrade.stdOut("ERROR: Binary GLIBC requirements incompatible with system", 0) + Upgrade.stdOut("This binary would cause OpenLiteSpeed to fail to start", 0) + Upgrade.stdOut("Skipping custom binary installation to preserve system stability", 0) + Upgrade.stdOut("Standard OLS binary from package manager will be used", 0) + Upgrade.stdOut("=" * 50, 0) + # Clean up downloaded binary + try: + if os.path.exists(tmp_binary): + os.remove(tmp_binary) + except: + pass + return True # Not fatal, continue with standard OLS + # Download module with checksum verification (if available) module_downloaded = False if MODULE_URL and MODULE_SHA256: @@ -826,11 +1030,31 @@ class Upgrade: Upgrade.stdOut("Installing custom binaries...", 0) try: + # Make binary executable before moving + os.chmod(tmp_binary, 0o755) + + # Final compatibility test before installation + if not Upgrade.testBinaryExecution(tmp_binary): + Upgrade.stdOut("ERROR: Final binary compatibility test failed", 0) + Upgrade.stdOut("Skipping installation to prevent OpenLiteSpeed failure", 0) + try: + if os.path.exists(tmp_binary): + os.remove(tmp_binary) + except: + pass + return True # Not fatal, continue with standard OLS + shutil.move(tmp_binary, OLS_BINARY_PATH) - os.chmod(OLS_BINARY_PATH, 0o755) Upgrade.stdOut("Installed OpenLiteSpeed binary", 0) except Exception as e: Upgrade.stdOut(f"ERROR: Failed to install binary: {e}", 0) + # Try to restore backup if installation failed + try: + if os.path.exists(f"{backup_dir}/openlitespeed.backup"): + shutil.copy2(f"{backup_dir}/openlitespeed.backup", OLS_BINARY_PATH) + Upgrade.stdOut("Restored original binary from backup", 0) + except: + pass return False # Install module (if downloaded)