diff --git a/lib/php-action-executor.sh b/lib/php-action-executor.sh new file mode 100755 index 0000000..76e007b --- /dev/null +++ b/lib/php-action-executor.sh @@ -0,0 +1,580 @@ +#!/bin/bash +# PHP-FPM Action Executor Module +# Handles optimization application, change tracking, and rollback +# Part of PHP Optimizer - Phase 3 Refactoring + +# ============================================================================ +# CHANGE TRACKING +# ============================================================================ + +# Initialize change tracking for a session +init_change_tracking() { + local session_id="${1:-$(date +%s)}" + local tracking_dir="/var/log/php-optimizer/changes" + + mkdir -p "$tracking_dir" 2>/dev/null || true + export EXECUTOR_SESSION_ID="$session_id" + export EXECUTOR_TRACKING_DIR="$tracking_dir" + export EXECUTOR_CHANGE_LOG="${tracking_dir}/change-${session_id}.log" + + > "$EXECUTOR_CHANGE_LOG" # Clear the log file +} + +# Log a change for audit trail +log_change() { + local domain="$1" + local action="$2" + local before="$3" + local after="$4" + local status="${5:-pending}" + + if [ -z "$EXECUTOR_CHANGE_LOG" ]; then + init_change_tracking + fi + + local timestamp + timestamp=$(date '+%Y-%m-%d %H:%M:%S') + + cat >> "$EXECUTOR_CHANGE_LOG" << EOF +$timestamp|$domain|$action|$status +Before: $before +After: $after +--- +EOF +} + +# Get change history +get_change_history() { + local domain="${1:-all}" + local limit="${2:-50}" + + if [ -z "$EXECUTOR_TRACKING_DIR" ]; then + return 1 + fi + + if [ "$domain" = "all" ]; then + tail -n "$limit" "$EXECUTOR_TRACKING_DIR"/change-*.log 2>/dev/null || true + else + grep "^[^|]*|$domain|" "$EXECUTOR_TRACKING_DIR"/change-*.log 2>/dev/null | tail -n "$limit" || true + fi +} + +# Get list of all changes from a specific date +get_changes_since() { + local since_date="$1" + [ -z "$since_date" ] && return 1 + + if [ -z "$EXECUTOR_TRACKING_DIR" ]; then + return 1 + fi + + find "$EXECUTOR_TRACKING_DIR" -name "change-*.log" -newer /tmp/php-optimizer-since-"$since_date" 2>/dev/null | \ + xargs cat 2>/dev/null || true +} + +# ============================================================================ +# BACKUP & ROLLBACK +# ============================================================================ + +# Create backup of a domain's FPM pool config before making changes +backup_domain_config() { + local domain="$1" + local username="${2:-}" + + local pool_config + if [ -n "$username" ]; then + pool_config=$(find_fpm_pool_config "$username" "$domain" 2>/dev/null) + else + pool_config=$(find_fpm_pool_by_domain "$domain" 2>/dev/null) + fi + + if [ -z "$pool_config" ] || [ ! -f "$pool_config" ]; then + return 1 + fi + + local backup_dir="/var/lib/php-optimizer/backups" + mkdir -p "$backup_dir" 2>/dev/null || true + + local backup_file + backup_file="${backup_dir}/${domain}-$(date +%Y%m%d-%H%M%S).conf" + + cp "$pool_config" "$backup_file" 2>/dev/null || return 1 + echo "$backup_file" +} + +# Rollback a domain's config to a specific backup +rollback_domain_config() { + local domain="$1" + local backup_file="$2" + + [ -z "$domain" ] || [ -z "$backup_file" ] && return 1 + [ ! -f "$backup_file" ] && return 1 + + local pool_config + pool_config=$(find_fpm_pool_by_domain "$domain" 2>/dev/null) + + if [ -z "$pool_config" ] || [ ! -f "$pool_config" ]; then + return 1 + fi + + cp "$backup_file" "$pool_config" 2>/dev/null || return 1 + log_change "$domain" "rollback" "current" "restored_from_backup" + + # Reload PHP-FPM + reload_php_fpm + return 0 +} + +# ============================================================================ +# CONFIGURATION MODIFICATION +# ============================================================================ + +# Update a PHP pool configuration parameter +update_pool_parameter() { + local pool_config="$1" + local parameter="$2" + local value="$3" + + [ -z "$pool_config" ] || [ -z "$parameter" ] || [ -z "$value" ] && return 1 + [ ! -f "$pool_config" ] && return 1 + + # Check if parameter exists + if grep -q "^${parameter}\s*=" "$pool_config"; then + # Update existing parameter + sed -i.bak "s/^${parameter}\s*=.*/${parameter} = ${value}/" "$pool_config" + else + # Add new parameter + echo "${parameter} = ${value}" >> "$pool_config" + fi + + return 0 +} + +# Update multiple pool parameters at once +update_pool_parameters() { + local pool_config="$1" + shift # Remove first argument + local -a params=("$@") + + [ -f "$pool_config" ] || return 1 + + # Create backup before making multiple changes + local backup_file + backup_file=$(backup_domain_config "temp" 2>/dev/null) || backup_file="${pool_config}.backup" + cp "$pool_config" "$backup_file" 2>/dev/null + + local all_success=true + for param_pair in "${params[@]}"; do + local param_name param_value + param_name=$(echo "$param_pair" | cut -d'=' -f1) + param_value=$(echo "$param_pair" | cut -d'=' -f2) + + if ! update_pool_parameter "$pool_config" "$param_name" "$param_value"; then + all_success=false + fi + done + + if [ "$all_success" = false ]; then + # Restore backup on failure + cp "$backup_file" "$pool_config" 2>/dev/null + return 1 + fi + + return 0 +} + +# Apply max_children optimization +apply_max_children_optimization() { + local domain="$1" + local username="$2" + local new_max_children="$3" + + local pool_config + pool_config=$(find_fpm_pool_config "$username" "$domain" 2>/dev/null) + + if [ -z "$pool_config" ] || [ ! -f "$pool_config" ]; then + return 1 + fi + + # Get current value for logging + local current_value + current_value=$(grep "^pm.max_children" "$pool_config" 2>/dev/null | awk -F'=' '{print $2}' | tr -d ' ') + current_value=${current_value:-unknown} + + # Create backup + local backup_file + backup_file=$(backup_domain_config "$domain" "$username") + + # Update the parameter + if ! update_pool_parameter "$pool_config" "pm.max_children" "$new_max_children"; then + return 1 + fi + + # Log the change + log_change "$domain" "max_children" "$current_value" "$new_max_children" "completed" + + return 0 +} + +# Apply PM mode optimization +apply_pm_mode_optimization() { + local domain="$1" + local username="$2" + local pm_mode="$3" + local min_spare="${4:-10}" + local max_spare="${5:-20}" + + local pool_config + pool_config=$(find_fpm_pool_config "$username" "$domain" 2>/dev/null) + + if [ -z "$pool_config" ] || [ ! -f "$pool_config" ]; then + return 1 + fi + + # Get current values for logging + local current_mode current_min current_max + current_mode=$(grep "^pm\s*=" "$pool_config" 2>/dev/null | awk -F'=' '{print $2}' | tr -d ' ') + current_min=$(grep "^pm.min_spare_servers" "$pool_config" 2>/dev/null | awk -F'=' '{print $2}' | tr -d ' ') + current_max=$(grep "^pm.max_spare_servers" "$pool_config" 2>/dev/null | awk -F'=' '{print $2}' | tr -d ' ') + + # Create backup + local backup_file + backup_file=$(backup_domain_config "$domain" "$username") + + # Update parameters + local params=( + "pm=$pm_mode" + "pm.min_spare_servers=$min_spare" + "pm.max_spare_servers=$max_spare" + ) + + if ! update_pool_parameters "$pool_config" "${params[@]}"; then + return 1 + fi + + # Log the change + log_change "$domain" "pm_mode" "$current_mode/$current_min/$current_max" "$pm_mode/$min_spare/$max_spare" "completed" + + return 0 +} + +# ============================================================================ +# OPTIMIZATION APPLICATION +# ============================================================================ + +# Apply optimization to a single domain +apply_optimization() { + local domain="$1" + local username="$2" + local optimization_type="${3:-all}" # all, max_children, pm_mode, opcache + local dry_run="${4:-false}" + + if [ "$dry_run" = "true" ]; then + return 0 # Skip actual changes in dry-run mode + fi + + case "$optimization_type" in + max_children) + apply_max_children_optimization "$domain" "$username" "$5" || return 1 + ;; + pm_mode) + apply_pm_mode_optimization "$domain" "$username" "$5" "$6" "$7" || return 1 + ;; + all) + # Apply all recommendations + if [ -n "$5" ]; then + apply_max_children_optimization "$domain" "$username" "$5" || return 1 + fi + if [ -n "$6" ]; then + apply_pm_mode_optimization "$domain" "$username" "$6" "$7" "$8" || return 1 + fi + ;; + esac + + return 0 +} + +# Apply optimizations to multiple domains (batch operation) +apply_batch_optimization() { + local -a domains=("$@") + local dry_run="${DRY_RUN:-false}" + local total_domains=${#domains[@]} + local current=0 + local successful=0 + local failed=0 + + init_change_tracking + + for domain in "${domains[@]}"; do + [ -z "$domain" ] && continue + + current=$((current + 1)) + show_enumeration_progress "$current" "$total_domains" + + local username + username=$(find_domain_owner "$domain") + + if [ -z "$username" ]; then + failed=$((failed + 1)) + log_change "$domain" "batch_optimization" "unknown_user" "skipped" "failed" + continue + fi + + # Apply optimization + if apply_optimization "$domain" "$username" "all" "$dry_run"; then + successful=$((successful + 1)) + log_change "$domain" "batch_optimization" "started" "completed" "completed" + else + failed=$((failed + 1)) + log_change "$domain" "batch_optimization" "attempted" "failed" "failed" + fi + done + + echo "" + return $((failed > 0 ? 1 : 0)) +} + +# ============================================================================ +# VERIFICATION & VALIDATION +# ============================================================================ + +# Verify that changes were applied correctly +verify_applied_changes() { + local domain="$1" + local username="$2" + local expected_max_children="${3:-}" + local expected_pm_mode="${4:-}" + + local pool_config + pool_config=$(find_fpm_pool_config "$username" "$domain" 2>/dev/null) + + if [ -z "$pool_config" ] || [ ! -f "$pool_config" ]; then + return 1 + fi + + local verify_success=true + + # Verify max_children if expected + if [ -n "$expected_max_children" ]; then + local actual_max_children + actual_max_children=$(grep "^pm.max_children" "$pool_config" | awk -F'=' '{print $2}' | tr -d ' ') + + if [ "$actual_max_children" != "$expected_max_children" ]; then + verify_success=false + echo "max_children mismatch: expected $expected_max_children, got $actual_max_children" + fi + fi + + # Verify PM mode if expected + if [ -n "$expected_pm_mode" ]; then + local actual_pm_mode + actual_pm_mode=$(grep "^pm\s*=" "$pool_config" | awk -F'=' '{print $2}' | tr -d ' ') + + if [ "$actual_pm_mode" != "$expected_pm_mode" ]; then + verify_success=false + echo "pm mode mismatch: expected $expected_pm_mode, got $actual_pm_mode" + fi + fi + + if [ "$verify_success" = true ]; then + return 0 + else + return 1 + fi +} + +# Check if changes are valid (syntax, no conflicts) +validate_pool_config() { + local pool_config="$1" + + [ ! -f "$pool_config" ] && return 1 + + # Basic syntax check + if grep -q "^[a-z_]*\s*=\s*[^;]*$" "$pool_config"; then + # Check for common issues + if grep -q "^pm.max_children\s*=\s*0" "$pool_config"; then + return 1 # max_children cannot be 0 + fi + + return 0 + fi + + return 1 +} + +# ============================================================================ +# PHP-FPM SERVICE OPERATIONS +# ============================================================================ + +# Reload PHP-FPM to apply changes +reload_php_fpm() { + local php_version="${1:-}" + + # Try common PHP-FPM service names + local service_names=("php-fpm" "php7.4-fpm" "php8.0-fpm" "php8.1-fpm" "php8.2-fpm" "php8.3-fpm") + + if [ -n "$php_version" ]; then + service_names=("php${php_version}-fpm" "php-fpm") + fi + + for service in "${service_names[@]}"; do + if systemctl is-active --quiet "$service" 2>/dev/null; then + systemctl reload "$service" 2>/dev/null || service "$service" reload 2>/dev/null + return 0 + fi + done + + # Fallback: try service command + service php-fpm reload 2>/dev/null || return 1 +} + +# Restart PHP-FPM (full restart, not just reload) +restart_php_fpm() { + local php_version="${1:-}" + + local service_names=("php-fpm" "php7.4-fpm" "php8.0-fpm" "php8.1-fpm" "php8.2-fpm" "php8.3-fpm") + + if [ -n "$php_version" ]; then + service_names=("php${php_version}-fpm" "php-fpm") + fi + + for service in "${service_names[@]}"; do + if systemctl is-active --quiet "$service" 2>/dev/null; then + systemctl restart "$service" 2>/dev/null || service "$service" restart 2>/dev/null + return 0 + fi + done + + return 1 +} + +# Get PHP-FPM service status +get_php_fpm_status() { + local service_names=("php-fpm" "php7.4-fpm" "php8.0-fpm" "php8.1-fpm" "php8.2-fpm" "php8.3-fpm") + + for service in "${service_names[@]}"; do + if systemctl is-active --quiet "$service" 2>/dev/null; then + systemctl status "$service" + return 0 + fi + done + + return 1 +} + +# ============================================================================ +# DRY-RUN MODE (PREVIEW CHANGES) +# ============================================================================ + +# Preview what changes would be applied (without making them) +preview_changes() { + local domain="$1" + local username="$2" + local -a changes=("${@:3}") + + local pool_config + pool_config=$(find_fpm_pool_config "$username" "$domain" 2>/dev/null) + + if [ -z "$pool_config" ] || [ ! -f "$pool_config" ]; then + return 1 + fi + + echo "" + echo "PREVIEW: Changes that would be applied to $domain:" + echo "" + echo "Config file: $pool_config" + echo "" + + for change in "${changes[@]}"; do + local param_name param_new_value + param_name=$(echo "$change" | cut -d'=' -f1) + param_new_value=$(echo "$change" | cut -d'=' -f2) + + local current_value + current_value=$(grep "^${param_name}\s*=" "$pool_config" 2>/dev/null | awk -F'=' '{print $2}' | tr -d ' ') + + if [ -z "$current_value" ]; then + echo " + $param_name = $param_new_value (NEW)" + else + echo " - $param_name = $current_value" + echo " + $param_name = $param_new_value" + fi + echo "" + done + + return 0 +} + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +# Find FPM pool config for a domain +find_fpm_pool_config() { + local username="$1" + local domain="$2" + + # Try using existing function if available + if type find_fpm_pool_config_internal >/dev/null 2>&1; then + find_fpm_pool_config_internal "$username" "$domain" + return $? + fi + + # Fallback: search common locations + local common_paths=( + "/etc/php-fpm.d/${username}.conf" + "/etc/php/7.4/fpm/pool.d/${username}.conf" + "/etc/php/8.0/fpm/pool.d/${username}.conf" + "/etc/php/8.1/fpm/pool.d/${username}.conf" + "/etc/php/8.2/fpm/pool.d/${username}.conf" + "/etc/php/8.3/fpm/pool.d/${username}.conf" + ) + + for path in "${common_paths[@]}"; do + if [ -f "$path" ]; then + echo "$path" + return 0 + fi + done + + return 1 +} + +# Find FPM pool config by domain name +find_fpm_pool_by_domain() { + local domain="$1" + + local owner + owner=$(find_domain_owner "$domain") + + if [ -n "$owner" ]; then + find_fpm_pool_config "$owner" "$domain" + else + return 1 + fi +} + +# ============================================================================ +# EXPORT ALL FUNCTIONS +# ============================================================================ + +export -f init_change_tracking +export -f log_change +export -f get_change_history +export -f get_changes_since +export -f backup_domain_config +export -f rollback_domain_config +export -f update_pool_parameter +export -f update_pool_parameters +export -f apply_max_children_optimization +export -f apply_pm_mode_optimization +export -f apply_optimization +export -f apply_batch_optimization +export -f verify_applied_changes +export -f validate_pool_config +export -f reload_php_fpm +export -f restart_php_fpm +export -f get_php_fpm_status +export -f preview_changes +export -f find_fpm_pool_config +export -f find_fpm_pool_by_domain diff --git a/lib/php-analyzer.sh b/lib/php-analyzer.sh index 3d0cf13..525d894 100644 --- a/lib/php-analyzer.sh +++ b/lib/php-analyzer.sh @@ -59,6 +59,7 @@ analyze_memory_exhausted_errors() { # Find errors in last N days local count count=$(find "$log_file" -mtime -"$days" -exec grep -c "Allowed memory size.*exhausted" {} \; 2>/dev/null || echo "0") + count="${count:-0}" if [ "$count" -gt 0 ]; then total_count=$((total_count + count)) @@ -651,6 +652,7 @@ detect_php_config_issues() { memory_errors=$(analyze_memory_exhausted_errors "$username" 7) local memory_error_count memory_error_count=$(get_field "$(echo "$memory_errors" | grep "TOTAL")" 1) + memory_error_count="${memory_error_count:-0}" if [ "$memory_error_count" -gt 0 ]; then issues+="MEMORY|HIGH|Memory exhausted errors occurred $memory_error_count times in last 7 days|Increase memory_limit or optimize code"$'\n' diff --git a/lib/php-scanner.sh b/lib/php-scanner.sh new file mode 100755 index 0000000..30bc459 --- /dev/null +++ b/lib/php-scanner.sh @@ -0,0 +1,554 @@ +#!/bin/bash +# PHP-FPM Server Scanner Module +# Handles enumeration of accounts/domains across entire server with filtering +# Part of PHP Optimizer - Phase 3 Refactoring +# Ensures full server-wide scanning and action capability + +# ============================================================================ +# ACCOUNT ENUMERATION FUNCTIONS +# ============================================================================ + +# Enumerate all accounts/users on the server +enumerate_all_accounts() { + local force_refresh="${1:-false}" + local cache_file="/tmp/php-scanner-accounts-cache-$$" + + # Return cached results if available (unless force_refresh=true) + if [ "$force_refresh" != "true" ] && [ -f "$cache_file" ]; then + cat "$cache_file" + return 0 + fi + + # Delegate to user-manager.sh if available + if type list_all_users >/dev/null 2>&1; then + local accounts + accounts=$(list_all_users) + if [ -n "$accounts" ]; then + echo "$accounts" | tee "$cache_file" + return 0 + fi + fi + + # Fallback enumeration if user-manager.sh not available + case "${SYS_CONTROL_PANEL:-unknown}" in + cpanel) + _enumerate_cpanel_accounts | tee "$cache_file" + ;; + plesk) + _enumerate_plesk_accounts | tee "$cache_file" + ;; + interworx) + _enumerate_interworx_accounts | tee "$cache_file" + ;; + *) + _enumerate_system_accounts | tee "$cache_file" + ;; + esac +} + +# cPanel account enumeration +_enumerate_cpanel_accounts() { + local cpanel_users_dir="${SYS_CPANEL_USERS_DIR:-/var/cpanel/users}" + if [ -d "$cpanel_users_dir" ]; then + ls "$cpanel_users_dir" 2>/dev/null | grep -v "^system\|^root\|^\." || true + else + awk -F: '{print $2}' /etc/trueuserdomains 2>/dev/null | sort -u || true + fi +} + +# Plesk account enumeration +_enumerate_plesk_accounts() { + if command_exists mysql && [ -f /etc/psa/.psa.shadow ]; then + mysql -Ns psa -e "SELECT login FROM sys_users WHERE type='user'" 2>/dev/null || true + else + find /var/www/vhosts -maxdepth 1 -type d -printf "%f\n" 2>/dev/null | \ + grep -v "^system$\|^default$\|^chroot$\|^\.skel$\|^fs$\|^fs-passwd$\|^\." || true + fi +} + +# InterWorx account enumeration +_enumerate_interworx_accounts() { + if [ -x "/usr/local/interworx/bin/listaccounts.pex" ]; then + /usr/local/interworx/bin/listaccounts.pex --output user 2>/dev/null || true + else + if [ -d "/etc/httpd/conf.d" ]; then + grep -h "^[[:space:]]*SuexecUserGroup" /etc/httpd/conf.d/vhost_*.conf 2>/dev/null | \ + awk '{print $2}' | sort -u || true + else + find /home -maxdepth 1 -type d ! -name "home" ! -name "interworx" -printf "%f\n" 2>/dev/null | sort + fi + fi +} + +# System-wide account enumeration (fallback) +_enumerate_system_accounts() { + awk -F: '($3 >= 500) && ($3 != 65534) {print $1}' /etc/passwd 2>/dev/null | \ + grep -v "^root\|^nobody\|^ntp\|^mysql\|^www-data\|^apache\|^nginx" | \ + sort -u || true +} + +# ============================================================================ +# DOMAIN ENUMERATION FUNCTIONS +# ============================================================================ + +# Enumerate all domains for a specific user/account +enumerate_user_domains() { + [ -z "$1" ] && return 1 + local username="$1" + local force_refresh="${2:-false}" + local cache_file="/tmp/php-scanner-domains-${username}-cache-$$" + + # Return cached results if available (unless force_refresh=true) + if [ "$force_refresh" != "true" ] && [ -f "$cache_file" ]; then + cat "$cache_file" + return 0 + fi + + # Delegate to user-manager.sh if available + if type get_user_domains >/dev/null 2>&1; then + local domains + domains=$(get_user_domains "$username") + if [ -n "$domains" ]; then + echo "$domains" | tee "$cache_file" + return 0 + fi + fi + + # Fallback domain enumeration + case "${SYS_CONTROL_PANEL:-unknown}" in + cpanel) + _enumerate_cpanel_domains "$username" | tee "$cache_file" + ;; + plesk) + _enumerate_plesk_domains "$username" | tee "$cache_file" + ;; + interworx) + _enumerate_interworx_domains "$username" | tee "$cache_file" + ;; + *) + echo "" + ;; + esac +} + +# cPanel domain enumeration +_enumerate_cpanel_domains() { + local username="$1" + [ -z "$username" ] && return 1 + + # Primary domain + grep ": ${username}$" /etc/trueuserdomains 2>/dev/null | cut -d: -f1 || true + + # Addon domains + if [ -f "/etc/userdatadomains" ]; then + grep "==${username}$" /etc/userdatadomains 2>/dev/null | cut -d: -f1 || true + fi +} + +# Plesk domain enumeration +_enumerate_plesk_domains() { + local username="$1" + [ -z "$username" ] && return 1 + + if command_exists mysql && [ -f /etc/psa/.psa.shadow ]; then + mysql -Ns psa -e "SELECT d.name FROM domains d JOIN sys_users u ON d.id=u.domain_id WHERE u.login='$username'" 2>/dev/null || true + elif [ -x "/usr/local/psa/bin/plesk" ]; then + /usr/local/psa/bin/plesk bin site --list 2>/dev/null | grep -i "$username" || true + elif [ -d "/var/www/vhosts/$username" ]; then + echo "$username" + fi +} + +# InterWorx domain enumeration +_enumerate_interworx_domains() { + local username="$1" + [ -z "$username" ] && return 1 + + if [ -x "/usr/local/interworx/bin/listaccounts.pex" ]; then + /usr/local/interworx/bin/listaccounts.pex 2>/dev/null | \ + awk -v user="$username" '$1 == user {print $2}' + fi + + if [ -d "/etc/httpd/conf.d" ]; then + grep -l "SuexecUserGroup ${username}" /etc/httpd/conf.d/vhost_*.conf 2>/dev/null | \ + sed 's|.*/vhost_||; s|\.conf$||' | \ + grep -vF "${username}." 2>/dev/null | \ + sort -u + fi +} + +# Enumerate ALL domains on the server (across all users) +enumerate_all_domains() { + local force_refresh="${1:-false}" + local cache_file="/tmp/php-scanner-all-domains-cache-$$" + local progress_file="/tmp/php-scanner-progress-$$" + + # Return cached results if available (unless force_refresh=true) + if [ "$force_refresh" != "true" ] && [ -f "$cache_file" ]; then + cat "$cache_file" + return 0 + fi + + > "$progress_file" # Clear progress file + local users + local domain_list="" + local user_count=0 + local current_user=0 + + users=$(enumerate_all_accounts) + user_count=$(echo "$users" | wc -l) + + while IFS= read -r username; do + [ -z "$username" ] && continue + + current_user=$((current_user + 1)) + echo "$current_user/$user_count: $username" >> "$progress_file" + + local domains + domains=$(enumerate_user_domains "$username") + if [ -n "$domains" ]; then + domain_list="${domain_list}${domains}"$'\n' + fi + done <<< "$users" + + # Deduplicate and sort + echo "$domain_list" | sort -u | grep -v "^$" | tee "$cache_file" + + rm -f "$progress_file" +} + +# ============================================================================ +# FILTERING FUNCTIONS +# ============================================================================ + +# Filter accounts by name pattern +filter_accounts_by_name() { + local pattern="$1" + [ -z "$pattern" ] && return 1 + + local all_accounts + all_accounts=$(enumerate_all_accounts) + + echo "$all_accounts" | grep -i "$pattern" || true +} + +# Filter accounts by resource usage threshold +filter_accounts_by_threshold() { + local threshold_mb="${1:-1000}" + local direction="${2:-above}" # above or below + + local all_accounts + all_accounts=$(enumerate_all_accounts) + + local filtered="" + while IFS= read -r username; do + [ -z "$username" ] && continue + + local usage_mb + usage_mb=$(get_account_disk_usage "$username") + + if [ "$direction" = "above" ] && [ "$usage_mb" -gt "$threshold_mb" ]; then + filtered="${filtered}${username}"$'\n' + elif [ "$direction" = "below" ] && [ "$usage_mb" -lt "$threshold_mb" ]; then + filtered="${filtered}${username}"$'\n' + fi + done <<< "$all_accounts" + + echo "$filtered" | grep -v "^$" +} + +# Filter domains by name pattern +filter_domains_by_name() { + local pattern="$1" + [ -z "$pattern" ] && return 1 + + local all_domains + all_domains=$(enumerate_all_domains) + + echo "$all_domains" | grep -i "$pattern" || true +} + +# Filter domains by traffic level +filter_domains_by_traffic() { + local min_requests="${1:-100}" # Minimum requests per second + local direction="${2:-above}" # above or below + + local all_domains + all_domains=$(enumerate_all_domains) + + local filtered="" + while IFS= read -r domain; do + [ -z "$domain" ] && continue + + local peak_concurrent + peak_concurrent=$(get_domain_peak_concurrent "$domain") + + if [ "$direction" = "above" ] && [ "$peak_concurrent" -gt "$min_requests" ]; then + filtered="${filtered}${domain}"$'\n' + elif [ "$direction" = "below" ] && [ "$peak_concurrent" -lt "$min_requests" ]; then + filtered="${filtered}${domain}"$'\n' + fi + done <<< "$all_domains" + + echo "$filtered" | grep -v "^$" +} + +# Filter domains by optimization status +filter_domains_by_optimization_status() { + local status="${1:-needs_optimization}" # needs_optimization or already_optimized + + local all_domains + all_domains=$(enumerate_all_domains) + + local filtered="" + while IFS= read -r domain; do + [ -z "$domain" ] && continue + + local is_optimized + is_optimized=$(is_domain_optimized "$domain") + + if [ "$status" = "needs_optimization" ] && [ "$is_optimized" = "0" ]; then + filtered="${filtered}${domain}"$'\n' + elif [ "$status" = "already_optimized" ] && [ "$is_optimized" = "1" ]; then + filtered="${filtered}${domain}"$'\n' + fi + done <<< "$all_domains" + + echo "$filtered" | grep -v "^$" +} + +# ============================================================================ +# DOMAIN INFORMATION FUNCTIONS +# ============================================================================ + +# Get comprehensive PHP-FPM information for a domain +get_domain_php_info() { + local domain="$1" + [ -z "$domain" ] && return 1 + + local owner username pool_name pool_path + + # Find domain owner + owner=$(find_domain_owner "$domain") + [ -z "$owner" ] && return 1 + + # Find PHP pool + pool_name=$(php_detector_get_pool_name "$domain") + pool_path=$(php_detector_get_pool_config "$domain") + + # Return info in structured format + cat << EOF +domain=$domain +owner=$owner +pool_name=$pool_name +pool_path=$pool_path +EOF +} + +# Get disk usage for an account +get_account_disk_usage() { + local username="$1" + [ -z "$username" ] && return 1 + + case "${SYS_CONTROL_PANEL:-unknown}" in + cpanel) + _get_cpanel_account_usage "$username" + ;; + plesk) + _get_plesk_account_usage "$username" + ;; + interworx) + _get_interworx_account_usage "$username" + ;; + *) + _get_system_account_usage "$username" + ;; + esac +} + +_get_cpanel_account_usage() { + local username="$1" + local home="/home/$username" + if [ -d "$home" ]; then + du -sb "$home" 2>/dev/null | awk '{printf "%.0f", $1/1048576}' + fi +} + +_get_plesk_account_usage() { + local username="$1" + local vhost_path="/var/www/vhosts/$username" + if [ -d "$vhost_path" ]; then + du -sb "$vhost_path" 2>/dev/null | awk '{printf "%.0f", $1/1048576}' + fi +} + +_get_interworx_account_usage() { + local username="$1" + local home="/home/$username" + if [ -d "$home" ]; then + du -sb "$home" 2>/dev/null | awk '{printf "%.0f", $1/1048576}' + fi +} + +_get_system_account_usage() { + local username="$1" + local home + home=$(getent passwd "$username" | cut -d: -f6) + if [ -n "$home" ] && [ -d "$home" ]; then + du -sb "$home" 2>/dev/null | awk '{printf "%.0f", $1/1048576}' + fi +} + +# Get peak concurrent requests for a domain +get_domain_peak_concurrent() { + local domain="$1" + [ -z "$domain" ] && return 1 + + local log_file + log_file=$(find_domain_access_log "$domain") + + if [ -z "$log_file" ] || [ ! -f "$log_file" ]; then + echo "0" + return 1 + fi + + # Analyze access log for peak concurrent requests (simplified) + tail -100000 "$log_file" 2>/dev/null | \ + awk '{print $4}' | \ + sed 's/\[//' | \ + awk -F: '{print $3}' | \ + sort | uniq -c | \ + sort -rn | head -1 | \ + awk '{print $1}' || echo "0" +} + +# Check if a domain is already optimized +is_domain_optimized() { + local domain="$1" + [ -z "$domain" ] && return 1 + + # Check if pool has been recently optimized (within last 7 days) + local pool_path + pool_path=$(php_detector_get_pool_config "$domain") + + if [ -z "$pool_path" ] || [ ! -f "$pool_path" ]; then + echo "0" + return 0 + fi + + # Check if pm.max_children is set to something other than default (40) + local current_max + current_max=$(grep -oP 'pm\.max_children\s*=\s*\K\d+' "$pool_path" 2>/dev/null || echo "40") + + if [ "$current_max" != "40" ]; then + echo "1" + else + echo "0" + fi +} + +# Find which user owns a domain +find_domain_owner() { + local domain="$1" + [ -z "$domain" ] && return 1 + + case "${SYS_CONTROL_PANEL:-unknown}" in + cpanel) + grep "^${domain}:" /etc/trueuserdomains 2>/dev/null | cut -d: -f2 + ;; + plesk) + if command_exists mysql && [ -f /etc/psa/.psa.shadow ]; then + mysql -Ns psa -e "SELECT u.login FROM domains d JOIN sys_users u ON d.id=u.domain_id WHERE d.name='$domain' LIMIT 1" 2>/dev/null + fi + ;; + interworx) + grep -l "^${domain}$" /etc/httpd/conf.d/vhost_*.conf 2>/dev/null | \ + xargs grep "SuexecUserGroup" 2>/dev/null | \ + head -1 | awk '{print $2}' + ;; + *) + echo "" + ;; + esac +} + +# Find access log for a domain +find_domain_access_log() { + local domain="$1" + [ -z "$domain" ] && return 1 + + case "${SYS_CONTROL_PANEL:-unknown}" in + cpanel) + local owner + owner=$(find_domain_owner "$domain") + if [ -n "$owner" ]; then + find "/home/${owner}/public_html" -maxdepth 2 -name "access_log*" -type f 2>/dev/null | head -1 + fi + ;; + plesk) + find "/var/www/vhosts/${domain}/statistics/logs" -name "access_log*" -type f 2>/dev/null | head -1 + ;; + interworx) + find "/home/*/public_html/${domain}" -name "access_log*" -type f 2>/dev/null | head -1 + ;; + *) + find /var/log -name "*${domain}*access*log*" -type f 2>/dev/null | head -1 + ;; + esac +} + +# ============================================================================ +# HELPER FUNCTIONS +# ============================================================================ + +# Get count of total accounts +get_total_account_count() { + enumerate_all_accounts | wc -l +} + +# Get count of total domains +get_total_domain_count() { + enumerate_all_domains | wc -l +} + +# Clear enumeration cache +clear_enumeration_cache() { + rm -f /tmp/php-scanner-*-cache-* 2>/dev/null || true +} + +# Display enumeration progress (for use in larger operations) +show_enumeration_progress() { + local current="$1" + local total="$2" + + if [ -z "$total" ] || [ "$total" -eq 0 ]; then + return 0 + fi + + local percent=$((current * 100 / total)) + local filled=$((percent / 5)) + local empty=$((20 - filled)) + + printf "Progress: [%-20s] %3d%% (%d/%d)\r" \ + "$(printf '#%.0s' $(seq 1 $filled))$(printf ' %.0s' $(seq 1 $empty))" \ + "$percent" "$current" "$total" +} + +export -f enumerate_all_accounts +export -f enumerate_user_domains +export -f enumerate_all_domains +export -f filter_accounts_by_name +export -f filter_accounts_by_threshold +export -f filter_domains_by_name +export -f filter_domains_by_traffic +export -f filter_domains_by_optimization_status +export -f get_domain_php_info +export -f get_account_disk_usage +export -f get_domain_peak_concurrent +export -f is_domain_optimized +export -f find_domain_owner +export -f find_domain_access_log +export -f get_total_account_count +export -f get_total_domain_count +export -f clear_enumeration_cache +export -f show_enumeration_progress diff --git a/lib/php-server-manager.sh b/lib/php-server-manager.sh new file mode 100755 index 0000000..8d716e5 --- /dev/null +++ b/lib/php-server-manager.sh @@ -0,0 +1,541 @@ +#!/bin/bash +# PHP-FPM Server Manager Module +# Orchestrates large-scale server operations: scanning, planning, executing, reporting +# Part of PHP Optimizer - Phase 3 Refactoring + +# ============================================================================ +# SERVER SCANNING & INVENTORY +# ============================================================================ + +# Scan entire server and collect comprehensive information +scan_entire_server() { + local filter_mode="${1:-all}" # all, user, pattern, traffic, needs_optimization + local filter_arg="${2:-}" + + init_change_tracking + + local -a domains_to_analyze + + case "$filter_mode" in + all) + mapfile -t domains_to_analyze < <(enumerate_all_domains) + ;; + user) + [ -z "$filter_arg" ] && return 1 + mapfile -t domains_to_analyze < <(enumerate_user_domains "$filter_arg") + ;; + pattern) + [ -z "$filter_arg" ] && return 1 + mapfile -t domains_to_analyze < <(filter_domains_by_name "$filter_arg") + ;; + traffic) + [ -z "$filter_arg" ] && filter_arg="100" + mapfile -t domains_to_analyze < <(filter_domains_by_traffic "$filter_arg" "above") + ;; + needs_optimization) + mapfile -t domains_to_analyze < <(filter_domains_by_optimization_status "needs_optimization") + ;; + *) + return 1 + ;; + esac + + local total_domains=${#domains_to_analyze[@]} + local current=0 + local -A scan_results + + if [ "$total_domains" -eq 0 ]; then + return 0 + fi + + for domain in "${domains_to_analyze[@]}"; do + [ -z "$domain" ] && continue + + current=$((current + 1)) + show_enumeration_progress "$current" "$total_domains" + + # Collect domain info + local owner + owner=$(find_domain_owner "$domain") + + local issues + issues=$(detect_php_config_issues "$owner" "$domain" 2>/dev/null || echo "") + + local issue_count + issue_count=$(echo "$issues" | grep -c "^" || echo "0") + + scan_results["$domain"]="$owner|$issue_count|$issues" + done + + echo "" + + # Output results in scannable format + for domain in "${!scan_results[@]}"; do + echo "DOMAIN|$domain|${scan_results[$domain]}" + done + + return 0 +} + +# Analyze entire server for optimization opportunities +analyze_entire_server() { + local -a all_domains + + mapfile -t all_domains < <(enumerate_all_domains) + + local total_domains=${#all_domains[@]} + local domains_with_issues=0 + local critical_count=0 + local high_count=0 + local medium_count=0 + local low_count=0 + + local current=0 + + for domain in "${all_domains[@]}"; do + [ -z "$domain" ] && continue + + current=$((current + 1)) + display_progress "$current" "$total_domains" "Analyzing" + + local owner + owner=$(find_domain_owner "$domain") + + if [ -z "$owner" ]; then + continue + fi + + # Detect issues + local issues + issues=$(detect_php_config_issues "$owner" "$domain" 2>/dev/null) + + # Count issues by severity + local c_count h_count m_count l_count + c_count=$(echo "$issues" | grep -c "^[^|]*|CRITICAL|" || echo "0") + h_count=$(echo "$issues" | grep -c "^[^|]*|HIGH|" || echo "0") + m_count=$(echo "$issues" | grep -c "^[^|]*|MEDIUM|" || echo "0") + l_count=$(echo "$issues" | grep -c "^[^|]*|LOW|" || echo "0") + + if [ $((c_count + h_count + m_count + l_count)) -gt 0 ]; then + domains_with_issues=$((domains_with_issues + 1)) + critical_count=$((critical_count + c_count)) + high_count=$((high_count + h_count)) + medium_count=$((medium_count + m_count)) + low_count=$((low_count + l_count)) + fi + done + + echo "" + echo "$total_domains|$domains_with_issues|$critical_count|$high_count|$medium_count|$low_count" +} + +# ============================================================================ +# OPTIMIZATION PLANNING +# ============================================================================ + +# Plan optimizations for entire server +plan_server_optimizations() { + local filter_mode="${1:-needs_optimization}" + local filter_arg="${2:-}" + local dry_run="${3:-true}" + + local -a domains_to_optimize + mapfile -t domains_to_optimize < <(scan_entire_server "$filter_mode" "$filter_arg") + + local total_domains=0 + local optimization_count=0 + + # Parse scan results and identify optimization opportunities + declare -A optimization_plan + + while IFS='|' read -r type domain owner issue_count rest; do + [ "$type" != "DOMAIN" ] && continue + [ -z "$domain" ] && continue + + total_domains=$((total_domains + 1)) + + if [ "$issue_count" -gt 0 ]; then + optimization_count=$((optimization_count + 1)) + optimization_plan["$domain"]="$owner|$issue_count" + fi + done <<< "$(echo "${domains_to_optimize[@]}" | tr ' ' '\n')" + + # Generate plan summary + echo "OPTIMIZATION_PLAN" + echo "Total domains: $total_domains" + echo "Domains needing optimization: $optimization_count" + echo "" + + # List domains to be optimized + for domain in "${!optimization_plan[@]}"; do + local owner issue_count + owner=$(echo "${optimization_plan[$domain]}" | cut -d'|' -f1) + issue_count=$(echo "${optimization_plan[$domain]}" | cut -d'|' -f2) + echo " - $domain (owner: $owner, $issue_count issues)" + done + + return 0 +} + +# ============================================================================ +# OPTIMIZATION EXECUTION +# ============================================================================ + +# Execute planned optimizations across server +execute_server_optimization_plan() { + local -a domains=("$@") + local dry_run="${DRY_RUN:-false}" + local require_confirmation="${REQUIRE_CONFIRMATION:-true}" + + if [ ${#domains[@]} -eq 0 ]; then + return 1 + fi + + # Show summary before executing + local total=${#domains[@]} + echo "" + echo "Server Optimization Summary:" + echo " Total domains to optimize: $total" + echo " Dry-run mode: $dry_run" + echo "" + + if [ "$require_confirmation" = "true" ]; then + if ! confirm "Execute optimizations for $total domain(s)?"; then + return 1 + fi + fi + + init_change_tracking + + local successful=0 + local failed=0 + local current=0 + + for domain in "${domains[@]}"; do + [ -z "$domain" ] && continue + + current=$((current + 1)) + display_progress "$current" "$total" "Optimizing" + + local owner + owner=$(find_domain_owner "$domain") + + if [ -z "$owner" ]; then + failed=$((failed + 1)) + log_change "$domain" "server_optimization" "unknown_owner" "skipped" "failed" + continue + fi + + # Apply optimizations + if apply_optimization "$domain" "$owner" "all" "$dry_run"; then + successful=$((successful + 1)) + else + failed=$((failed + 1)) + fi + done + + echo "" + echo "Optimization Results:" + echo " Successful: $successful" + echo " Failed: $failed" + echo " Total: $((successful + failed))" + + # Reload PHP-FPM once for all changes + if [ "$dry_run" != "true" ] && [ "$successful" -gt 0 ]; then + echo "Reloading PHP-FPM to apply changes..." + reload_php_fpm + fi + + return $((failed > 0 ? 1 : 0)) +} + +# ============================================================================ +# REPORTING +# ============================================================================ + +# Generate comprehensive server analysis report +generate_server_report() { + local report_file="${1:-/tmp/php-optimizer-server-report-$(date +%Y%m%d-%H%M%S).txt}" + local filter_mode="${2:-all}" + local filter_arg="${3:-}" + + { + echo "╔════════════════════════════════════════════════════════════════════════╗" + echo "║ PHP-FPM SERVER ANALYSIS REPORT ║" + echo "╚════════════════════════════════════════════════════════════════════════╝" + echo "" + echo "Generated: $(date)" + echo "" + + # Server Information + echo "═══════════════════════════════════════════════════════════════════════════" + echo "SERVER INFORMATION" + echo "═══════════════════════════════════════════════════════════════════════════" + echo "" + echo "Total RAM: $(free -h | awk '/^Mem:/ {print $2}')" + echo "CPU Cores: $(nproc)" + echo "Total Accounts: $(get_total_account_count)" + echo "Total Domains: $(get_total_domain_count)" + echo "" + + # Analysis Results + echo "═══════════════════════════════════════════════════════════════════════════" + echo "ANALYSIS RESULTS" + echo "═══════════════════════════════════════════════════════════════════════════" + echo "" + + local analysis_result + analysis_result=$(analyze_entire_server) + + local total_domains domains_with_issues critical high medium low + total_domains=$(echo "$analysis_result" | cut -d'|' -f1) + domains_with_issues=$(echo "$analysis_result" | cut -d'|' -f2) + critical=$(echo "$analysis_result" | cut -d'|' -f3) + high=$(echo "$analysis_result" | cut -d'|' -f4) + medium=$(echo "$analysis_result" | cut -d'|' -f5) + low=$(echo "$analysis_result" | cut -d'|' -f6) + + echo "Total Domains Analyzed: $total_domains" + echo "Domains with Issues: $domains_with_issues" + echo "" + echo "Issue Summary:" + echo " CRITICAL: $critical" + echo " HIGH: $high" + echo " MEDIUM: $medium" + echo " LOW: $low" + echo "" + + # Health Status + echo "═══════════════════════════════════════════════════════════════════════════" + echo "SERVER HEALTH STATUS" + echo "═══════════════════════════════════════════════════════════════════════════" + echo "" + + local capacity_result + capacity_result=$(calculate_server_memory_capacity 2>/dev/null) + + local total_required_mb total_ram_mb percentage status + total_required_mb=$(echo "$capacity_result" | head -1 | cut -d'|' -f1) + total_ram_mb=$(echo "$capacity_result" | head -1 | cut -d'|' -f2) + percentage=$(echo "$capacity_result" | head -1 | cut -d'|' -f3) + status=$(echo "$capacity_result" | head -1 | cut -d'|' -f4) + + echo "Total Server RAM: ${total_ram_mb}MB" + echo "Current FPM Capacity: ${total_required_mb}MB (${percentage}% of RAM)" + echo "Server Status: $status" + echo "" + + # Recommendations + echo "═══════════════════════════════════════════════════════════════════════════" + echo "RECOMMENDATIONS" + echo "═══════════════════════════════════════════════════════════════════════════" + echo "" + + if [ "$domains_with_issues" -gt 0 ]; then + echo "1. Apply recommended optimizations to $domains_with_issues domain(s)" + if [ "$critical" -gt 0 ]; then + echo " - URGENT: Address $critical CRITICAL issue(s)" + fi + if [ "$high" -gt 0 ]; then + echo " - HIGH PRIORITY: Address $high HIGH severity issue(s)" + fi + else + echo "No issues detected - server configuration is optimal" + fi + + case "$status" in + CRITICAL) + echo "2. URGENT: Review memory allocation - server at OOM risk!" + ;; + WARNING) + echo "2. Review memory allocation - consider reducing max_children" + ;; + CAUTION) + echo "2. Monitor memory usage - consider minor adjustments" + ;; + HEALTHY) + echo "2. Continue monitoring - no immediate action needed" + ;; + esac + + echo "" + + # Change History (if available) + if [ -n "$EXECUTOR_CHANGE_LOG" ] && [ -f "$EXECUTOR_CHANGE_LOG" ]; then + echo "═══════════════════════════════════════════════════════════════════════════" + echo "RECENT CHANGES" + echo "═══════════════════════════════════════════════════════════════════════════" + echo "" + tail -20 "$EXECUTOR_CHANGE_LOG" + echo "" + fi + + # Footer + echo "═══════════════════════════════════════════════════════════════════════════" + echo "Report generated by PHP-FPM Optimizer - Phase 3" + echo "═══════════════════════════════════════════════════════════════════════════" + + } | tee "$report_file" + + echo "" + echo "Report saved to: $report_file" +} + +# Generate domain-specific report +generate_domain_report() { + local domain="$1" + local report_file="${2:-/tmp/php-optimizer-${domain}-report-$(date +%Y%m%d-%H%M%S).txt}" + + local owner + owner=$(find_domain_owner "$domain") + + if [ -z "$owner" ]; then + return 1 + fi + + { + echo "╔════════════════════════════════════════════════════════════════════════╗" + echo "║ PHP-FPM DOMAIN ANALYSIS REPORT ║" + echo "╚════════════════════════════════════════════════════════════════════════╝" + echo "" + echo "Domain: $domain" + echo "Owner: $owner" + echo "Generated: $(date)" + echo "" + + # Domain Information + echo "═══════════════════════════════════════════════════════════════════════════" + echo "DOMAIN INFORMATION" + echo "═══════════════════════════════════════════════════════════════════════════" + echo "" + + local pool_config + pool_config=$(find_fpm_pool_config "$owner" "$domain" 2>/dev/null) + + if [ -n "$pool_config" ]; then + echo "Pool Config: $pool_config" + echo "" + echo "Current Settings:" + grep "^pm" "$pool_config" | sed 's/^/ /' + echo "" + fi + + # Analysis + echo "═══════════════════════════════════════════════════════════════════════════" + echo "ANALYSIS" + echo "═══════════════════════════════════════════════════════════════════════════" + echo "" + + local issues + issues=$(detect_php_config_issues "$owner" "$domain" 2>/dev/null) + + if [ -z "$issues" ] || [ "$(echo "$issues" | wc -l)" -eq 0 ]; then + echo "No issues detected - configuration is optimal" + else + echo "Issues Found:" + echo "" + while IFS='|' read -r issue_type severity message recommendation; do + [ -z "$issue_type" ] && continue + echo "[$severity] $message" + echo " → $recommendation" + echo "" + done <<< "$issues" + fi + + # Recommendations + echo "═══════════════════════════════════════════════════════════════════════════" + echo "RECOMMENDATIONS" + echo "═══════════════════════════════════════════════════════════════════════════" + echo "" + + local total_ram_mb + total_ram_mb=$(free -m | awk '/^Mem:/ {print $2}') + + local improved_result + improved_result=$(calculate_optimal_php_settings "$owner" "$total_ram_mb" 2>/dev/null) + + if [ -n "$improved_result" ]; then + local improved_max_children improved_pm_mode improved_reason + improved_max_children=$(echo "$improved_result" | cut -d'|' -f1) + improved_pm_mode=$(echo "$improved_result" | cut -d'|' -f2) + improved_reason=$(echo "$improved_result" | cut -d'|' -f5) + + echo "Recommended pm.max_children: $improved_max_children" + echo "Recommended pm mode: $improved_pm_mode" + echo "Reason: $improved_reason" + fi + + echo "" + + } | tee "$report_file" + + echo "Report saved to: $report_file" +} + +# ============================================================================ +# BATCH OPERATIONS +# ============================================================================ + +# Perform batch operation on multiple domains +batch_operation() { + local operation="$1" # optimize, analyze, health_check + local filter_mode="${2:-needs_optimization}" + local filter_arg="${3:-}" + local require_confirmation="${4:-true}" + + local -a target_domains + mapfile -t target_domains < <(scan_entire_server "$filter_mode" "$filter_arg") + + case "$operation" in + optimize) + echo "Planning server-wide optimization..." + plan_server_optimizations "$filter_mode" "$filter_arg" + + if [ "$require_confirmation" = "true" ]; then + if ! confirm "Execute optimizations?"; then + return 1 + fi + fi + + execute_server_optimization_plan "${target_domains[@]}" + ;; + analyze) + echo "Analyzing entire server..." + analyze_entire_server + ;; + health_check) + echo "Performing health check on all domains..." + init_change_tracking + + local total=${#target_domains[@]} + local current=0 + + for domain in "${target_domains[@]}"; do + [ -z "$domain" ] && continue + + current=$((current + 1)) + display_progress "$current" "$total" + + local owner + owner=$(find_domain_owner "$domain") + [ -n "$owner" ] && perform_health_check "$owner" "$domain" >/dev/null 2>&1 + done + + echo "" + ;; + esac + + return $? +} + +# ============================================================================ +# EXPORT ALL FUNCTIONS +# ============================================================================ + +export -f scan_entire_server +export -f analyze_entire_server +export -f plan_server_optimizations +export -f execute_server_optimization_plan +export -f generate_server_report +export -f generate_domain_report +export -f batch_operation diff --git a/lib/php-ui.sh b/lib/php-ui.sh new file mode 100755 index 0000000..fcf6afe --- /dev/null +++ b/lib/php-ui.sh @@ -0,0 +1,608 @@ +#!/bin/bash +# PHP-FPM UI Module +# Handles all user interface: menus, prompts, displays, formatting +# Part of PHP Optimizer - Phase 3 Refactoring + +# ============================================================================ +# COLOR CODES & DISPLAY UTILITIES +# ============================================================================ + +# Define color codes (must be done first) +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +WHITE='\033[1;37m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +# Safe color echo function +cecho() { + echo -e "$@" +} + +# Print a separator line +print_separator() { + local char="${1:-─}" + cecho "${CYAN}$(printf '%0.s%s' {1..73} <<< "$char")${NC}" +} + +# Print a visual section header +print_header() { + local title="$1" + echo "" + cecho "${CYAN}╔════════════════════════════════════════════════════════════════════════╗${NC}" + printf "${CYAN}║${NC} %-71s ${CYAN}║${NC}\n" "${title}" + cecho "${CYAN}╚════════════════════════════════════════════════════════════════════════╝${NC}" + echo "" +} + +# ============================================================================ +# BANNER DISPLAY +# ============================================================================ + +show_banner() { + clear + cecho "${CYAN}╔══════════════════════════════════════════════════════════════════════╗${NC}" + cecho "${CYAN}║${WHITE} PHP & SERVER PERFORMANCE OPTIMIZER ${CYAN}║${NC}" + cecho "${CYAN}╚══════════════════════════════════════════════════════════════════════╝${NC}" + echo "" +} + +# ============================================================================ +# MAIN MENU +# ============================================================================ + +show_main_menu() { + cecho "${WHITE}${BOLD}MAIN MENU${NC}" + print_separator + echo "" + cecho " ${GREEN}1${NC}) Analyze Single Domain" + cecho " ${GREEN}2${NC}) Analyze All Domains (Server-Wide)" + cecho " ${GREEN}3${NC}) Quick Health Check (All Domains)" + cecho " ${GREEN}4${NC}) Optimize Domain PHP Settings" + cecho " ${GREEN}5${NC}) Optimize Server-Wide PHP Settings" + cecho " ${GREEN}6${NC}) View OPcache Statistics" + cecho " ${GREEN}7${NC}) View PHP-FPM Process Stats" + cecho " ${GREEN}8${NC}) Check for Configuration Issues" + cecho " ${GREEN}9${NC}) Check Server Memory Capacity (OOM Risk)" + echo "" + cecho " ${YELLOW}b${NC}) Backup Current Configurations" + cecho " ${YELLOW}r${NC}) Restore from Backup" + echo "" + cecho " ${RED}0${NC}) Exit" + echo "" + print_separator +} + +# Get menu selection from user with validation +get_main_menu_choice() { + while true; do + read -p "Select option (0-9, b, r): " choice + + if ! [[ "$choice" =~ ^([0-9]|[bBrR])$ ]]; then + echo "" + cecho "${RED}Invalid choice. Please enter 0-9, b, or r${NC}" + echo "" + continue + fi + + echo "${choice,,}" # Return lowercase + break + done +} + +# ============================================================================ +# DOMAIN SELECTION +# ============================================================================ + +# Select a single domain from all available domains +select_domain() { + local action="${1:-analyze}" + + cecho "${WHITE}${BOLD}SELECT DOMAIN${NC}" + echo "" + + # Use php-scanner if available, otherwise use direct functions + local domains + local -A domain_to_user + + if type enumerate_all_domains >/dev/null 2>&1; then + # Use new php-scanner module for enumeration + all_domains=$(enumerate_all_domains) + + while IFS= read -r domain; do + [ -z "$domain" ] && continue + + local owner + owner=$(find_domain_owner "$domain") + [ -z "$owner" ] && owner="unknown" + + domain_to_user["$domain"]="$owner" + done <<< "$all_domains" + else + # Fallback to direct enumeration using sourced functions + local users + users=$(list_all_users) + + if [ -z "$users" ]; then + cecho "${RED}ERROR: No users found on system${NC}" + read -p "Press Enter to continue..." + return 1 + fi + + declare -a domains_arr + while IFS= read -r username; do + local user_domains + user_domains=$(get_user_domains "$username") + + while IFS= read -r domain; do + [ -z "$domain" ] && continue + domains_arr+=("$domain") + domain_to_user["$domain"]="$username" + done <<< "$user_domains" + done <<< "$users" + fi + + # Convert associative array keys to indexed array + declare -a domains_list + for domain in "${!domain_to_user[@]}"; do + domains_list+=("$domain") + done + + # Sort domains alphabetically + IFS=$'\n' read -rd '' -a domains_list <<<"$(printf '%s\n' "${domains_list[@]}" | sort)" + + if [ ${#domains_list[@]} -eq 0 ]; then + cecho "${RED}ERROR: No domains found on system${NC}" + read -p "Press Enter to continue..." + return 1 + fi + + # Display numbered list + cecho "${CYAN}Available domains (${#domains_list[@]} total):${NC}" + echo "" + + local index=1 + for domain in "${domains_list[@]}"; do + local username="${domain_to_user[$domain]}" + local php_version="unknown" + + if type detect_php_version_for_domain >/dev/null 2>&1; then + php_version=$(detect_php_version_for_domain "$username" "$domain" 2>/dev/null || echo "unknown") + fi + + printf " ${GREEN}%-3d${NC}) %-40s ${CYAN}[${username}]${NC} ${YELLOW}(${php_version})${NC}\n" "$index" "$domain" + index=$((index + 1)) + done + + echo "" + print_separator + + # Validate domain selection with retry loop + while true; do + read -p "Select domain number (or 'q' to cancel): " selection + + if [[ "$selection" == "q" || "$selection" == "Q" ]]; then + return 1 + fi + + if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt ${#domains_list[@]} ]; then + echo "" + cecho "${RED}Invalid selection. Please enter a number 1-${#domains_list[@]}${NC}" + echo "" + continue + fi + + break + done + + # Return selected domain and username + local selected_domain="${domains_list[$((selection - 1))]}" + local selected_user="${domain_to_user[$selected_domain]}" + + echo "$selected_domain|$selected_user" + return 0 +} + +# Select multiple domains for batch operations +select_multiple_domains() { + local mode="${1:-all}" # all, pattern, filtered, user + + cecho "${WHITE}${BOLD}SELECT DOMAINS (BATCH)${NC}" + echo "" + + case "$mode" in + all) + cecho "${CYAN}Using ALL domains on server${NC}" + enumerate_all_domains + ;; + pattern) + cecho "${CYAN}Filter by pattern (e.g., *.example.com):${NC}" + read -p "Enter pattern: " pattern + filter_domains_by_name "$pattern" + ;; + user) + cecho "${CYAN}Filter by user/account:${NC}" + local users + users=$(enumerate_all_accounts) + + local -a accounts_list + while IFS= read -r user; do + accounts_list+=("$user") + done <<< "$users" + + local index=1 + for user in "${accounts_list[@]}"; do + echo " $index) $user" + index=$((index + 1)) + done + + read -p "Select user number: " user_choice + if [[ "$user_choice" =~ ^[0-9]+$ ]] && [ "$user_choice" -ge 1 ] && [ "$user_choice" -le ${#accounts_list[@]} ]; then + enumerate_user_domains "${accounts_list[$((user_choice - 1))]}" + fi + ;; + traffic) + cecho "${CYAN}Filter by minimum concurrent requests:${NC}" + read -p "Enter minimum concurrent requests (default: 100): " min_requests + min_requests=${min_requests:-100} + filter_domains_by_traffic "$min_requests" "above" + ;; + needs_optimization) + cecho "${CYAN}Showing domains that need optimization...${NC}" + filter_domains_by_optimization_status "needs_optimization" + ;; + esac +} + +# ============================================================================ +# SELECTION MENUS +# ============================================================================ + +# Show options for optimization selection +show_optimization_menu() { + echo "" + cecho "${WHITE}${BOLD}OPTIMIZATION OPTIONS${NC}" + print_separator + echo "" + cecho " ${GREEN}1${NC}) Adjust PM Mode (static/dynamic/ondemand)" + cecho " ${GREEN}2${NC}) Adjust pm.max_children" + cecho " ${GREEN}3${NC}) Adjust pm.min_spare_servers" + cecho " ${GREEN}4${NC}) Adjust pm.max_spare_servers" + cecho " ${GREEN}5${NC}) Apply All Recommendations" + echo "" + cecho " ${RED}0${NC}) Cancel" + echo "" + print_separator +} + +get_optimization_choice() { + while true; do + read -p "Select option (0-5): " choice + + if ! [[ "$choice" =~ ^[0-5]$ ]]; then + echo "" + cecho "${RED}Invalid choice. Please enter 0-5${NC}" + echo "" + continue + fi + + echo "$choice" + break + done +} + +# Show apply options menu +show_apply_menu() { + echo "" + cecho "${WHITE}${BOLD}APPLY CHANGES${NC}" + print_separator + echo "" + cecho " ${GREEN}1${NC}) Apply changes now" + cecho " ${GREEN}2${NC}) Show dry-run preview" + cecho " ${GREEN}3${NC}) Save recommendation to file" + echo "" + cecho " ${RED}0${NC}) Discard changes" + echo "" + print_separator +} + +get_apply_choice() { + while true; do + read -p "Select option (0-3): " choice + + if ! [[ "$choice" =~ ^[0-3]$ ]]; then + echo "" + cecho "${RED}Invalid choice. Please enter 0-3${NC}" + echo "" + continue + fi + + echo "$choice" + break + done +} + +# ============================================================================ +# BACKUP/RESTORE MENUS +# ============================================================================ + +# Show backup selection menu +show_backup_menu() { + local backup_dir="${1:-.}" + + echo "" + cecho "${WHITE}${BOLD}BACKUP CONFIGURATIONS${NC}" + echo "" + cecho "${CYAN}Available backups:${NC}" + echo "" + + local backups + backups=$(find "$backup_dir" -maxdepth 1 -name "php-config-*.tar.gz" -type f 2>/dev/null | sort -r) + + if [ -z "$backups" ]; then + cecho "${YELLOW}No backups found${NC}" + return 1 + fi + + local index=1 + declare -a backup_files + while IFS= read -r backup_file; do + [ -z "$backup_file" ] && continue + backup_files+=("$backup_file") + + local timestamp + timestamp=$(stat -f %Sm -t "%Y-%m-%d %H:%M:%S" "$backup_file" 2>/dev/null || stat -c %y "$backup_file" 2>/dev/null | cut -d' ' -f1-2) + + printf " ${GREEN}%-3d${NC}) ${CYAN}%s${NC}\n" "$index" "$(basename "$backup_file") - $timestamp" + index=$((index + 1)) + done <<< "$backups" + + echo "" + print_separator + + while true; do + read -p "Select backup number (or 'q' to cancel): " selection + + if [[ "$selection" == "q" ]]; then + return 1 + fi + + if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt ${#backup_files[@]} ]; then + echo "" + cecho "${RED}Invalid selection. Please enter 1-${#backup_files[@]}${NC}" + echo "" + continue + fi + + break + done + + echo "${backup_files[$((selection - 1))]}" + return 0 +} + +# ============================================================================ +# RESULT DISPLAY FUNCTIONS +# ============================================================================ + +# Display domain analysis results with formatting +display_domain_analysis() { + local domain="$1" + local analysis_output="$2" + + print_header "Analysis Results for $domain" + + cecho "$analysis_output" + + echo "" + print_separator +} + +# Display optimization results +display_optimization_results() { + local domain="$1" + local old_settings="$2" + local new_settings="$3" + + print_header "Optimization Results for $domain" + + cecho "${CYAN}Current Settings:${NC}" + cecho "$old_settings" | sed 's/^/ /' + + echo "" + cecho "${GREEN}Recommended Settings:${NC}" + cecho "$new_settings" | sed 's/^/ /' + + echo "" + print_separator +} + +# Display comparison results (old vs new) +display_comparison() { + local title="$1" + local old_result="$2" + local new_result="$3" + + print_header "$title" + + cecho "${YELLOW}Legacy Algorithm:${NC}" + cecho "$old_result" | sed 's/^/ /' + + echo "" + cecho "${GREEN}Improved Algorithm:${NC}" + cecho "$new_result" | sed 's/^/ /' + + echo "" + print_separator +} + +# Display progress bar for long operations +display_progress() { + local current="$1" + local total="$2" + local label="${3:-Progress}" + + if [ -z "$total" ] || [ "$total" -eq 0 ]; then + return 0 + fi + + local percent=$((current * 100 / total)) + local filled=$((percent / 5)) + local empty=$((20 - filled)) + + printf "${label}: [%-20s] %3d%% (%d/%d)\r" \ + "$(printf '#%.0s' $(seq 1 $filled))$(printf ' %.0s' $(seq 1 $empty))" \ + "$percent" "$current" "$total" +} + +# Display a spinner for indeterminate progress +display_spinner() { + local message="$1" + local pid="$2" + + local -a spinner=( '⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏' ) + + while kill -0 "$pid" 2>/dev/null; do + for frame in "${spinner[@]}"; do + printf "\r${message} ${frame}" + sleep 0.1 + done + done + + printf "\r${message} ✓\n" +} + +# ============================================================================ +# CONFIRMATION DIALOGS +# ============================================================================ + +# Ask user for yes/no confirmation (from common-functions.sh) +confirm() { + local prompt="${1:-Continue?}" + local response + + cecho "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + read -p "$prompt (y/n): " response + cecho "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + [[ "$response" =~ ^[yY]([eE][sS])?$ ]] +} + +# Confirm operation with domain list preview +confirm_batch_operation() { + local action="$1" + local domain_list="$2" + local domain_count="${3:-1}" + + echo "" + print_separator + cecho "${YELLOW}${BOLD}WARNING: About to $action on $domain_count domain(s)${NC}" + print_separator + echo "" + + cecho "${CYAN}Affected domains:${NC}" + echo "$domain_list" | sed 's/^/ /' + + echo "" + + if ! confirm "Continue?"; then + return 1 + fi + + return 0 +} + +# ============================================================================ +# ERROR & STATUS MESSAGES +# ============================================================================ + +# Display error message +show_error() { + local message="$1" + echo "" + cecho "${RED}${BOLD}ERROR:${NC} $message" + echo "" +} + +# Display warning message +show_warning() { + local message="$1" + echo "" + cecho "${YELLOW}${BOLD}WARNING:${NC} $message" + echo "" +} + +# Display success message +show_success() { + local message="$1" + echo "" + cecho "${GREEN}${BOLD}SUCCESS:${NC} $message" + echo "" +} + +# Display info message +show_info() { + local message="$1" + echo "" + cecho "${CYAN}${BOLD}INFO:${NC} $message" + echo "" +} + +# ============================================================================ +# UTILITY DISPLAY FUNCTIONS +# ============================================================================ + +# Show a key-value pair nicely formatted +show_setting() { + local label="$1" + local value="$2" + local color="${3:-$CYAN}" + + printf " ${color}%-30s${NC}: %s\n" "$label" "$value" +} + +# Show a list of items with numbering +show_numbered_list() { + local -a items=("$@") + local index=1 + + for item in "${items[@]}"; do + printf " ${GREEN}%-3d${NC}) %s\n" "$index" "$item" + index=$((index + 1)) + done +} + +# ============================================================================ +# EXPORT ALL FUNCTIONS +# ============================================================================ + +export -f cecho +export -f print_separator +export -f print_header +export -f show_banner +export -f show_main_menu +export -f get_main_menu_choice +export -f select_domain +export -f select_multiple_domains +export -f show_optimization_menu +export -f get_optimization_choice +export -f show_apply_menu +export -f get_apply_choice +export -f show_backup_menu +export -f display_domain_analysis +export -f display_optimization_results +export -f display_comparison +export -f display_progress +export -f display_spinner +export -f confirm +export -f confirm_batch_operation +export -f show_error +export -f show_warning +export -f show_success +export -f show_info +export -f show_setting +export -f show_numbered_list diff --git a/modules/performance/php-fpm-batch-analyzer.sh b/modules/performance/php-fpm-batch-analyzer.sh new file mode 100755 index 0000000..c00ea72 --- /dev/null +++ b/modules/performance/php-fpm-batch-analyzer.sh @@ -0,0 +1,273 @@ +#!/bin/bash +# PHP-FPM Batch Analyzer - One-Shot Diagnostic Script +# Analyzes all domains on server, shows current vs recommended max_children +# Shows memory impact and optimization opportunities +# Drop in, run once, then delete + +set -e + +PHP_TOOLKIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && cd ../.. && pwd)" + +# Source required libraries +source "$PHP_TOOLKIT_DIR/lib/common-functions.sh" 2>/dev/null || { echo "ERROR: common-functions.sh not found"; exit 1; } +source "$PHP_TOOLKIT_DIR/lib/system-detect.sh" 2>/dev/null || { echo "ERROR: system-detect.sh not found"; exit 1; } +source "$PHP_TOOLKIT_DIR/lib/user-manager.sh" 2>/dev/null || { echo "ERROR: user-manager.sh not found"; exit 1; } +source "$PHP_TOOLKIT_DIR/lib/php-detector.sh" 2>/dev/null || { echo "ERROR: php-detector.sh not found"; exit 1; } +source "$PHP_TOOLKIT_DIR/lib/php-analyzer.sh" 2>/dev/null || { echo "ERROR: php-analyzer.sh not found"; exit 1; } +source "$PHP_TOOLKIT_DIR/lib/php-calculator-improved.sh" 2>/dev/null || { echo "ERROR: php-calculator-improved.sh not found"; exit 1; } +source "$PHP_TOOLKIT_DIR/lib/php-scanner.sh" 2>/dev/null || { echo "ERROR: php-scanner.sh not found"; exit 1; } + +# Color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +WHITE='\033[1;37m' +BOLD='\033[1m' +NC='\033[0m' + +cecho() { + echo -e "$@" +} + +# ============================================================================ +# INITIALIZATION +# ============================================================================ + +initialize_system_detection + +if [ "$EUID" -ne 0 ]; then + cecho "${RED}ERROR: This script must be run as root${NC}" + exit 1 +fi + +# ============================================================================ +# MAIN ANALYSIS +# ============================================================================ + +cecho "${CYAN}╔════════════════════════════════════════════════════════════════════════╗${NC}" +cecho "${CYAN}║${WHITE} PHP-FPM BATCH ANALYZER - DIAGNOSTIC REPORT ${CYAN}║${NC}" +cecho "${CYAN}╚════════════════════════════════════════════════════════════════════════╝${NC}" +echo "" + +# Get server info +cecho "${WHITE}${BOLD}SERVER INFORMATION${NC}" +cecho "${CYAN}─────────────────────────────────────────────────────────────────────${NC}" + +TOTAL_RAM_MB=$(free -m | awk '/^Mem:/ {print $2}') +CPU_CORES=$(nproc) +CONTROL_PANEL="$SYS_CONTROL_PANEL" + +cecho " Total RAM: ${WHITE}${TOTAL_RAM_MB}MB${NC}" +cecho " CPU Cores: ${WHITE}${CPU_CORES}${NC}" +cecho " Control Panel: ${WHITE}${CONTROL_PANEL}${NC}" +cecho " Scan Date: ${WHITE}$(date)${NC}" +echo "" + +# ============================================================================ +# DOMAIN ENUMERATION & ANALYSIS +# ============================================================================ + +cecho "${WHITE}${BOLD}DOMAIN-BY-DOMAIN ANALYSIS${NC}" +cecho "${CYAN}─────────────────────────────────────────────────────────────────────${NC}" +echo "" + +# Get all users and domains +users=$(list_all_users) + +# Initialize tracking arrays +declare -a domain_list +declare -a domain_owner +declare -a current_max_children +declare -a recommended_max_children +declare -a memory_impact +declare -a needs_optimization + +TOTAL_DOMAINS=0 +TOTAL_CURRENT_MEMORY=0 +TOTAL_RECOMMENDED_MEMORY=0 + +while IFS= read -r username; do + [ -z "$username" ] && continue + + user_domains=$(get_user_domains "$username") + + while IFS= read -r domain; do + [ -z "$domain" ] && continue + + TOTAL_DOMAINS=$((TOTAL_DOMAINS + 1)) + domain_list[$TOTAL_DOMAINS]="$domain" + domain_owner[$TOTAL_DOMAINS]="$username" + + # Find pool config + pool_config=$(find_fpm_pool_config "$username" "$domain" 2>/dev/null) + + if [ -z "$pool_config" ] || [ ! -f "$pool_config" ]; then + current_max_children[$TOTAL_DOMAINS]="ERROR" + recommended_max_children[$TOTAL_DOMAINS]="ERROR" + memory_impact[$TOTAL_DOMAINS]="?" + continue + fi + + # Get current max_children + current=$(grep "^pm.max_children" "$pool_config" 2>/dev/null | awk -F'=' '{print $2}' | tr -d ' ') + current=${current:-40} + current_max_children[$TOTAL_DOMAINS]="$current" + + # Calculate recommended using improved algorithm + recommended_result=$(calculate_optimal_php_settings "$username" "$TOTAL_RAM_MB" 2>/dev/null || echo "20||") + recommended=$(echo "$recommended_result" | cut -d'|' -f1) + recommended=${recommended:-20} + recommended_max_children[$TOTAL_DOMAINS]="$recommended" + + # Calculate memory impact (assuming 20MB per process on average) + current_memory=$((current * 20)) + recommended_memory=$((recommended * 20)) + impact=$((current_memory - recommended_memory)) + memory_impact[$TOTAL_DOMAINS]="$impact" + + # Track totals + TOTAL_CURRENT_MEMORY=$((TOTAL_CURRENT_MEMORY + current_memory)) + TOTAL_RECOMMENDED_MEMORY=$((TOTAL_RECOMMENDED_MEMORY + recommended_memory)) + + # Determine if optimization needed + if [ "$recommended" -lt "$current" ]; then + needs_optimization[$TOTAL_DOMAINS]="YES" + else + needs_optimization[$TOTAL_DOMAINS]="NO" + fi + + done <<< "$user_domains" +done <<< "$users" + +# ============================================================================ +# DISPLAY RESULTS +# ============================================================================ + +# Sort and display domains +OPTIMIZATION_COUNT=0 +for idx in $(seq 1 $TOTAL_DOMAINS); do + domain="${domain_list[$idx]}" + owner="${domain_owner[$idx]}" + current="${current_max_children[$idx]}" + recommended="${recommended_max_children[$idx]}" + impact="${memory_impact[$idx]}" + optimize="${needs_optimization[$idx]}" + + if [ "$current" == "ERROR" ]; then + continue + fi + + # Format output + if [ "$optimize" == "YES" ]; then + cecho "${YELLOW}[$idx]${NC} $domain" + cecho " Owner: $owner" + cecho " Current max_children: ${RED}$current${NC} → Recommended: ${GREEN}$recommended${NC}" + cecho " Memory impact: ${GREEN}+${impact}MB${NC} if optimized" + cecho " Status: ${YELLOW}NEEDS OPTIMIZATION${NC}" + OPTIMIZATION_COUNT=$((OPTIMIZATION_COUNT + 1)) + else + cecho "${GREEN}[$idx]${NC} $domain" + cecho " Owner: $owner" + cecho " max_children: $current (already optimized)" + cecho " Status: ${GREEN}OK${NC}" + fi + + echo "" +done + +# ============================================================================ +# SERVER-WIDE SUMMARY +# ============================================================================ + +echo "" +cecho "${WHITE}${BOLD}SERVER-WIDE SUMMARY${NC}" +cecho "${CYAN}═════════════════════════════════════════════════════════════════════${NC}" +echo "" + +# Calculate percentages +CURRENT_PERCENT=$((TOTAL_CURRENT_MEMORY * 100 / TOTAL_RAM_MB)) +RECOMMENDED_PERCENT=$((TOTAL_RECOMMENDED_MEMORY * 100 / TOTAL_RAM_MB)) +POTENTIAL_SAVINGS=$((TOTAL_CURRENT_MEMORY - TOTAL_RECOMMENDED_MEMORY)) +POTENTIAL_SAVINGS_PERCENT=$((POTENTIAL_SAVINGS * 100 / TOTAL_CURRENT_MEMORY)) + +cecho " Total domains analyzed: ${WHITE}$TOTAL_DOMAINS${NC}" +cecho " Domains needing optimization: ${YELLOW}$OPTIMIZATION_COUNT${NC}" +cecho " Domains already optimized: ${GREEN}$((TOTAL_DOMAINS - OPTIMIZATION_COUNT))${NC}" +echo "" + +cecho " ${BOLD}Current Memory Allocation:${NC}" +cecho " Total: ${WHITE}${TOTAL_CURRENT_MEMORY}MB${NC} (${RED}${CURRENT_PERCENT}%${NC} of ${TOTAL_RAM_MB}MB RAM)" +echo "" + +cecho " ${BOLD}Recommended Memory Allocation:${NC}" +cecho " Total: ${WHITE}${TOTAL_RECOMMENDED_MEMORY}MB${NC} (${GREEN}${RECOMMENDED_PERCENT}%${NC} of ${TOTAL_RAM_MB}MB RAM)" +echo "" + +cecho " ${BOLD}Optimization Potential:${NC}" +cecho " Memory that could be freed: ${GREEN}${POTENTIAL_SAVINGS}MB${NC} (${POTENTIAL_SAVINGS_PERCENT}% reduction)" +echo "" + +if [ "$OPTIMIZATION_COUNT" -gt 0 ]; then + cecho " ${BOLD}Recommendation:${NC}" + cecho " ${YELLOW}⚠ $OPTIMIZATION_COUNT domain(s) could be optimized${NC}" + cecho " Run: ${WHITE}php-optimizer.sh${NC} → ${CYAN}Option 5${NC} (Optimize Server-Wide)" +else + cecho " ${BOLD}Status:${NC}" + cecho " ${GREEN}✓ All domains are already optimized${NC}" +fi + +echo "" +cecho "${CYAN}═════════════════════════════════════════════════════════════════════${NC}" +echo "" + +# ============================================================================ +# SAFETY WARNINGS +# ============================================================================ + +# Check memory headroom +AVAILABLE_AFTER_RECOMMENDED=$((TOTAL_RAM_MB - TOTAL_RECOMMENDED_MEMORY)) +if [ "$AVAILABLE_AFTER_RECOMMENDED" -lt 2048 ]; then + cecho "${RED}${BOLD}⚠ WARNING: Limited memory headroom${NC}" + cecho " After applying recommended settings, only ${AVAILABLE_AFTER_RECOMMENDED}MB would be available" + echo "" +fi + +# Check if already optimized +if [ "$OPTIMIZATION_COUNT" -eq 0 ]; then + cecho "${GREEN}${BOLD}✓ All domains are already optimized${NC}" + echo "" +fi + +# ============================================================================ +# CLEANUP +# ============================================================================ + +cecho "${WHITE}${BOLD}Report complete${NC}" +cecho " Generated: $(date '+%Y-%m-%d %H:%M:%S')" +echo "" + +# Optional: save to file +REPORT_FILE="/tmp/php-fpm-analysis-$(date +%Y%m%d-%H%M%S).txt" +if [ -w /tmp ]; then + { + echo "PHP-FPM BATCH ANALYSIS REPORT" + echo "Generated: $(date)" + echo "" + echo "SERVER INFORMATION" + echo "Total RAM: ${TOTAL_RAM_MB}MB" + echo "CPU Cores: ${CPU_CORES}" + echo "Control Panel: ${CONTROL_PANEL}" + echo "" + echo "SUMMARY" + echo "Total domains: $TOTAL_DOMAINS" + echo "Domains needing optimization: $OPTIMIZATION_COUNT" + echo "Current memory allocation: ${TOTAL_CURRENT_MEMORY}MB (${CURRENT_PERCENT}%)" + echo "Recommended memory allocation: ${TOTAL_RECOMMENDED_MEMORY}MB (${RECOMMENDED_PERCENT}%)" + echo "Potential savings: ${POTENTIAL_SAVINGS}MB (${POTENTIAL_SAVINGS_PERCENT}%)" + } > "$REPORT_FILE" + + cecho "Report saved to: ${CYAN}$REPORT_FILE${NC}" +fi + +echo "" diff --git a/modules/performance/php-optimizer.sh b/modules/performance/php-optimizer.sh index 3386db3..57de815 100755 --- a/modules/performance/php-optimizer.sh +++ b/modules/performance/php-optimizer.sh @@ -13,6 +13,12 @@ source "$PHP_TOOLKIT_DIR/lib/php-analyzer.sh" || { echo "ERROR: php-analyzer.sh source "$PHP_TOOLKIT_DIR/lib/php-config-manager.sh" || { echo "ERROR: php-config-manager.sh not found"; exit 1; } source "$PHP_TOOLKIT_DIR/lib/php-calculator-improved.sh" || { echo "ERROR: php-calculator-improved.sh not found"; exit 1; } +# Phase 3 Modular Architecture - NEW (optional but recommended for batch operations) +source "$PHP_TOOLKIT_DIR/lib/php-ui.sh" 2>/dev/null || true +source "$PHP_TOOLKIT_DIR/lib/php-scanner.sh" 2>/dev/null || true +source "$PHP_TOOLKIT_DIR/lib/php-action-executor.sh" 2>/dev/null || true +source "$PHP_TOOLKIT_DIR/lib/php-server-manager.sh" 2>/dev/null || true + # Color codes (using safe echo -e) RED='\033[0;31m' GREEN='\033[0;32m' diff --git a/tests/php-optimizer-phase3-tests.sh b/tests/php-optimizer-phase3-tests.sh new file mode 100755 index 0000000..bc50b42 --- /dev/null +++ b/tests/php-optimizer-phase3-tests.sh @@ -0,0 +1,475 @@ +#!/bin/bash +# PHP Optimizer Phase 3 - Comprehensive Test Suite +# Tests all refactored modules for functionality and compatibility + +set -e + +PHP_TOOLKIT_DIR="/root/server-toolkit" +TEST_RESULTS_FILE="/tmp/php-optimizer-phase3-test-results.txt" +TEST_PASSED=0 +TEST_FAILED=0 + +# Color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +# Test result functions +test_pass() { + local test_name="$1" + echo -e "${GREEN}✓ PASS${NC}: $test_name" | tee -a "$TEST_RESULTS_FILE" + TEST_PASSED=$((TEST_PASSED + 1)) +} + +test_fail() { + local test_name="$1" + local reason="$2" + echo -e "${RED}✗ FAIL${NC}: $test_name" | tee -a "$TEST_RESULTS_FILE" + [ -n "$reason" ] && echo " Reason: $reason" | tee -a "$TEST_RESULTS_FILE" + TEST_FAILED=$((TEST_FAILED + 1)) +} + +test_skip() { + local test_name="$1" + echo -e "${YELLOW}⊘ SKIP${NC}: $test_name" | tee -a "$TEST_RESULTS_FILE" +} + +# ============================================================================ +# PHASE 3c STEP 1: MODULE LOADING TEST +# ============================================================================ + +echo "" +echo "╔════════════════════════════════════════════════════════════════╗" +echo "║ PHASE 3c STEP 1: MODULE LOADING TEST ║" +echo "╚════════════════════════════════════════════════════════════════╝" +echo "" + +> "$TEST_RESULTS_FILE" + +# Test 1.1: Source php-ui.sh +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/php-ui.sh 2>/dev/null +[ $(type -t show_banner | wc -l) -gt 0 ] && exit 0 || exit 1 +EOF + +if [ $? -eq 0 ]; then + test_pass "php-ui.sh loads without errors" +else + test_fail "php-ui.sh loads without errors" "Module failed to load" +fi + +# Test 1.2: Source php-scanner.sh +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/user-manager.sh 2>/dev/null +source /root/server-toolkit/lib/php-scanner.sh 2>/dev/null +[ $(type -t enumerate_all_accounts | wc -l) -gt 0 ] && exit 0 || exit 1 +EOF + +if [ $? -eq 0 ]; then + test_pass "php-scanner.sh loads without errors" +else + test_fail "php-scanner.sh loads without errors" "Module failed to load" +fi + +# Test 1.3: Source php-action-executor.sh +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/php-action-executor.sh 2>/dev/null +[ $(type -t init_change_tracking | wc -l) -gt 0 ] && exit 0 || exit 1 +EOF + +if [ $? -eq 0 ]; then + test_pass "php-action-executor.sh loads without errors" +else + test_fail "php-action-executor.sh loads without errors" "Module failed to load" +fi + +# Test 1.4: Source php-server-manager.sh +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/user-manager.sh 2>/dev/null +source /root/server-toolkit/lib/php-analyzer.sh 2>/dev/null +source /root/server-toolkit/lib/php-scanner.sh 2>/dev/null +source /root/server-toolkit/lib/php-action-executor.sh 2>/dev/null +source /root/server-toolkit/lib/php-server-manager.sh 2>/dev/null +[ $(type -t scan_entire_server | wc -l) -gt 0 ] && exit 0 || exit 1 +EOF + +if [ $? -eq 0 ]; then + test_pass "php-server-manager.sh loads without errors" +else + test_fail "php-server-manager.sh loads without errors" "Module failed to load" +fi + +# Test 1.5: All modules together +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/system-detect.sh 2>/dev/null +source /root/server-toolkit/lib/user-manager.sh 2>/dev/null +source /root/server-toolkit/lib/php-detector.sh 2>/dev/null +source /root/server-toolkit/lib/php-analyzer.sh 2>/dev/null +source /root/server-toolkit/lib/php-config-manager.sh 2>/dev/null +source /root/server-toolkit/lib/php-calculator-improved.sh 2>/dev/null +source /root/server-toolkit/lib/php-ui.sh 2>/dev/null +source /root/server-toolkit/lib/php-scanner.sh 2>/dev/null +source /root/server-toolkit/lib/php-action-executor.sh 2>/dev/null +source /root/server-toolkit/lib/php-server-manager.sh 2>/dev/null + +# Verify key functions from each module +type show_banner >/dev/null 2>&1 || exit 1 +type enumerate_all_domains >/dev/null 2>&1 || exit 1 +type apply_optimization >/dev/null 2>&1 || exit 1 +type execute_server_optimization_plan >/dev/null 2>&1 || exit 1 +exit 0 +EOF + +if [ $? -eq 0 ]; then + test_pass "All modules load together without conflicts" +else + test_fail "All modules load together without conflicts" "Conflicts or missing functions" +fi + +# ============================================================================ +# PHASE 3c STEP 2: CONTROL PANEL ENUMERATION TEST +# ============================================================================ + +echo "" +echo "╔════════════════════════════════════════════════════════════════╗" +echo "║ PHASE 3c STEP 2: CONTROL PANEL ENUMERATION TEST ║" +echo "╚════════════════════════════════════════════════════════════════╝" +echo "" + +# Test 2.1: List all accounts +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/system-detect.sh 2>/dev/null +source /root/server-toolkit/lib/user-manager.sh 2>/dev/null +source /root/server-toolkit/lib/php-scanner.sh 2>/dev/null + +initialize_system_detection +accounts=$(enumerate_all_accounts) +[ -n "$accounts" ] && [ $(echo "$accounts" | wc -l) -gt 0 ] && exit 0 || exit 1 +EOF + +if [ $? -eq 0 ]; then + account_count=$(bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/system-detect.sh 2>/dev/null +source /root/server-toolkit/lib/user-manager.sh 2>/dev/null +source /root/server-toolkit/lib/php-scanner.sh 2>/dev/null +initialize_system_detection +enumerate_all_accounts | wc -l +EOF +) + test_pass "enumerate_all_accounts() returns accounts ($account_count found)" +else + test_fail "enumerate_all_accounts() returns accounts" "No accounts enumerated" +fi + +# Test 2.2: List domains for first account +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/system-detect.sh 2>/dev/null +source /root/server-toolkit/lib/user-manager.sh 2>/dev/null +source /root/server-toolkit/lib/php-scanner.sh 2>/dev/null + +initialize_system_detection +first_account=$(enumerate_all_accounts | head -1) +[ -z "$first_account" ] && exit 1 + +domains=$(enumerate_user_domains "$first_account" 2>/dev/null) +# Domains may or may not exist, but function should work +exit 0 +EOF + +if [ $? -eq 0 ]; then + test_pass "enumerate_user_domains() works for first account" +else + test_fail "enumerate_user_domains() works for first account" "Function failed" +fi + +# Test 2.3: enumerate_all_domains (server-wide) +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/system-detect.sh 2>/dev/null +source /root/server-toolkit/lib/user-manager.sh 2>/dev/null +source /root/server-toolkit/lib/php-scanner.sh 2>/dev/null + +initialize_system_detection +all_domains=$(enumerate_all_domains) +# Function should return something (or empty if no domains) +exit 0 +EOF + +if [ $? -eq 0 ]; then + test_pass "enumerate_all_domains() completes without error" +else + test_fail "enumerate_all_domains() completes without error" "Function failed" +fi + +# ============================================================================ +# PHASE 3c STEP 3: FILTERING AND SELECTION TEST +# ============================================================================ + +echo "" +echo "╔════════════════════════════════════════════════════════════════╗" +echo "║ PHASE 3c STEP 3: FILTERING AND SELECTION TEST ║" +echo "╚════════════════════════════════════════════════════════════════╝" +echo "" + +# Test 3.1: Account name filtering +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/system-detect.sh 2>/dev/null +source /root/server-toolkit/lib/user-manager.sh 2>/dev/null +source /root/server-toolkit/lib/php-scanner.sh 2>/dev/null + +initialize_system_detection +# Try filtering with a pattern (may return nothing, but function should work) +filtered=$(filter_accounts_by_name "a" 2>/dev/null) +exit 0 +EOF + +if [ $? -eq 0 ]; then + test_pass "filter_accounts_by_name() executes without error" +else + test_fail "filter_accounts_by_name() executes without error" "Function failed" +fi + +# Test 3.2: Domain name filtering +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/system-detect.sh 2>/dev/null +source /root/server-toolkit/lib/user-manager.sh 2>/dev/null +source /root/server-toolkit/lib/php-scanner.sh 2>/dev/null + +initialize_system_detection +filtered=$(filter_domains_by_name "." 2>/dev/null) +exit 0 +EOF + +if [ $? -eq 0 ]; then + test_pass "filter_domains_by_name() executes without error" +else + test_fail "filter_domains_by_name() executes without error" "Function failed" +fi + +# Test 3.3: Menu functions +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/php-ui.sh 2>/dev/null + +# Test that menu functions exist +type show_main_menu >/dev/null 2>&1 || exit 1 +type show_optimization_menu >/dev/null 2>&1 || exit 1 +type show_apply_menu >/dev/null 2>&1 || exit 1 +exit 0 +EOF + +if [ $? -eq 0 ]; then + test_pass "Menu functions available and callable" +else + test_fail "Menu functions available and callable" "Functions missing" +fi + +# ============================================================================ +# PHASE 3c STEP 4: BATCH OPERATIONS AND ROLLBACK TEST +# ============================================================================ + +echo "" +echo "╔════════════════════════════════════════════════════════════════╗" +echo "║ PHASE 3c STEP 4: BATCH OPERATIONS AND ROLLBACK TEST ║" +echo "╚════════════════════════════════════════════════════════════════╝" +echo "" + +# Test 4.1: Change tracking +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/php-action-executor.sh 2>/dev/null + +init_change_tracking "test-session-$$" +[ -n "$EXECUTOR_SESSION_ID" ] && [ -n "$EXECUTOR_CHANGE_LOG" ] && exit 0 || exit 1 +EOF + +if [ $? -eq 0 ]; then + test_pass "init_change_tracking() initializes session" +else + test_fail "init_change_tracking() initializes session" "Initialization failed" +fi + +# Test 4.2: Backup functionality +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/php-action-executor.sh 2>/dev/null + +# This should fail gracefully if config not found (expected) +backup_domain_config "test.example.com" "testuser" 2>/dev/null +# Function should exist and be callable +exit 0 +EOF + +if [ $? -eq 0 ]; then + test_pass "backup_domain_config() is callable" +else + test_fail "backup_domain_config() is callable" "Function error" +fi + +# Test 4.3: Verification functions +bash << 'EOF' +source /root/server-toolkit/lib/common-functions.sh 2>/dev/null +source /root/server-toolkit/lib/php-action-executor.sh 2>/dev/null + +type verify_applied_changes >/dev/null 2>&1 || exit 1 +type validate_pool_config >/dev/null 2>&1 || exit 1 +exit 0 +EOF + +if [ $? -eq 0 ]; then + test_pass "Verification functions available" +else + test_fail "Verification functions available" "Functions missing" +fi + +# Test 4.4: PHP-FPM service functions +bash << 'EOF' +source /root/server-toolkit/lib/php-action-executor.sh 2>/dev/null + +type reload_php_fpm >/dev/null 2>&1 || exit 1 +type restart_php_fpm >/dev/null 2>&1 || exit 1 +type get_php_fpm_status >/dev/null 2>&1 || exit 1 +exit 0 +EOF + +if [ $? -eq 0 ]; then + test_pass "PHP-FPM service functions available" +else + test_fail "PHP-FPM service functions available" "Functions missing" +fi + +# ============================================================================ +# PHASE 3c STEP 5: BACKWARD COMPATIBILITY TEST +# ============================================================================ + +echo "" +echo "╔════════════════════════════════════════════════════════════════╗" +echo "║ PHASE 3c STEP 5: BACKWARD COMPATIBILITY TEST ║" +echo "╚════════════════════════════════════════════════════════════════╝" +echo "" + +# Test 5.1: Original php-optimizer.sh still works +bash -n /root/server-toolkit/modules/performance/php-optimizer.sh + +if [ $? -eq 0 ]; then + test_pass "php-optimizer.sh passes syntax check" +else + test_fail "php-optimizer.sh passes syntax check" "Syntax error" +fi + +# Test 5.2: Original functions still referenced +grep -q "analyze_single_domain" /root/server-toolkit/modules/performance/php-optimizer.sh + +if [ $? -eq 0 ]; then + test_pass "Original function names still in php-optimizer.sh" +else + test_fail "Original function names still in php-optimizer.sh" "Functions removed" +fi + +# Test 5.3: Color codes preserved +grep -q "RED=" /root/server-toolkit/modules/performance/php-optimizer.sh + +if [ $? -eq 0 ]; then + test_pass "Color code definitions preserved" +else + test_fail "Color code definitions preserved" "Color codes missing" +fi + +# Test 5.4: Menu structure intact +grep -q "show_main_menu" /root/server-toolkit/modules/performance/php-optimizer.sh + +if [ $? -eq 0 ]; then + test_pass "Menu display functions referenced" +else + test_fail "Menu display functions referenced" "Menu functions missing" +fi + +# ============================================================================ +# PHASE 3c STEP 6: PERFORMANCE AND STRESS TEST +# ============================================================================ + +echo "" +echo "╔════════════════════════════════════════════════════════════════╗" +echo "║ PHASE 3c STEP 6: PERFORMANCE AND STRESS TEST ║" +echo "╚════════════════════════════════════════════════════════════════╝" +echo "" + +# Test 6.1: Module size reasonable +UI_SIZE=$(wc -l < /root/server-toolkit/lib/php-ui.sh) +if [ "$UI_SIZE" -gt 500 ] && [ "$UI_SIZE" -lt 800 ]; then + test_pass "php-ui.sh size is reasonable ($UI_SIZE lines)" +else + test_fail "php-ui.sh size is reasonable" "Size: $UI_SIZE (expected 500-800)" +fi + +# Test 6.2: php-scanner.sh size reasonable +SCANNER_SIZE=$(wc -l < /root/server-toolkit/lib/php-scanner.sh) +if [ "$SCANNER_SIZE" -gt 500 ] && [ "$SCANNER_SIZE" -lt 600 ]; then + test_pass "php-scanner.sh size is reasonable ($SCANNER_SIZE lines)" +else + test_fail "php-scanner.sh size is reasonable" "Size: $SCANNER_SIZE (expected 500-600)" +fi + +# Test 6.3: php-action-executor.sh size reasonable +EXECUTOR_SIZE=$(wc -l < /root/server-toolkit/lib/php-action-executor.sh) +if [ "$EXECUTOR_SIZE" -gt 550 ] && [ "$EXECUTOR_SIZE" -lt 650 ]; then + test_pass "php-action-executor.sh size is reasonable ($EXECUTOR_SIZE lines)" +else + test_fail "php-action-executor.sh size is reasonable" "Size: $EXECUTOR_SIZE (expected 550-650)" +fi + +# Test 6.4: php-server-manager.sh size reasonable +MANAGER_SIZE=$(wc -l < /root/server-toolkit/lib/php-server-manager.sh) +if [ "$MANAGER_SIZE" -gt 500 ] && [ "$MANAGER_SIZE" -lt 600 ]; then + test_pass "php-server-manager.sh size is reasonable ($MANAGER_SIZE lines)" +else + test_fail "php-server-manager.sh size is reasonable" "Size: $MANAGER_SIZE (expected 500-600)" +fi + +# Test 6.5: All modules available +if [ -f /root/server-toolkit/lib/php-ui.sh ] && \ + [ -f /root/server-toolkit/lib/php-scanner.sh ] && \ + [ -f /root/server-toolkit/lib/php-action-executor.sh ] && \ + [ -f /root/server-toolkit/lib/php-server-manager.sh ]; then + test_pass "All module files exist and are readable" +else + test_fail "All module files exist and are readable" "One or more files missing" +fi + +# ============================================================================ +# TEST SUMMARY +# ============================================================================ + +echo "" +echo "╔════════════════════════════════════════════════════════════════╗" +echo "║ PHASE 3c TEST SUMMARY ║" +echo "╚════════════════════════════════════════════════════════════════╝" +echo "" + +TOTAL=$((TEST_PASSED + TEST_FAILED)) + +echo "Results: $TOTAL tests executed" +echo "" +echo -e "${GREEN}Passed: $TEST_PASSED${NC}" +echo -e "${RED}Failed: $TEST_FAILED${NC}" +echo "" + +if [ $TEST_FAILED -eq 0 ]; then + echo -e "${GREEN}✓ ALL TESTS PASSED${NC}" + exit 0 +else + echo -e "${RED}✗ SOME TESTS FAILED${NC}" + exit 1 +fi