1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 14:26:46 +00:00
Files
Mole/bin/optimize.sh

517 lines
15 KiB
Bash
Executable File

#!/bin/bash
# Mole - Optimize command.
# Runs system maintenance checks and fixes.
# Supports dry-run where applicable.
set -euo pipefail
# Fix locale issues.
export LC_ALL=C
export LANG=C
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$SCRIPT_DIR/lib/core/common.sh"
# Clean temp files on exit.
trap cleanup_temp_files EXIT INT TERM
source "$SCRIPT_DIR/lib/core/sudo.sh"
source "$SCRIPT_DIR/lib/manage/update.sh"
source "$SCRIPT_DIR/lib/manage/autofix.sh"
source "$SCRIPT_DIR/lib/optimize/maintenance.sh"
source "$SCRIPT_DIR/lib/optimize/tasks.sh"
source "$SCRIPT_DIR/lib/check/health_json.sh"
source "$SCRIPT_DIR/lib/check/all.sh"
source "$SCRIPT_DIR/lib/manage/whitelist.sh"
print_header() {
printf '\n'
echo -e "${PURPLE_BOLD}Optimize and Check${NC}"
}
run_system_checks() {
# Skip checks in dry-run mode.
if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
return 0
fi
unset AUTO_FIX_SUMMARY AUTO_FIX_DETAILS
unset MOLE_SECURITY_FIXES_SHOWN
unset MOLE_SECURITY_FIXES_SKIPPED
echo ""
check_all_updates
echo ""
check_system_health
echo ""
check_all_security
if ask_for_security_fixes; then
perform_security_fixes
fi
if [[ "${MOLE_SECURITY_FIXES_SKIPPED:-}" != "true" ]]; then
echo ""
fi
check_all_config
echo ""
show_suggestions
if ask_for_updates; then
perform_updates
fi
if ask_for_auto_fix; then
perform_auto_fix
fi
}
show_optimization_summary() {
local safe_count="${OPTIMIZE_SAFE_COUNT:-0}"
local confirm_count="${OPTIMIZE_CONFIRM_COUNT:-0}"
if ((safe_count == 0 && confirm_count == 0)) && [[ -z "${AUTO_FIX_SUMMARY:-}" ]]; then
return
fi
local summary_title
local -a summary_details=()
local total_applied=$((safe_count + confirm_count))
if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
summary_title="Dry Run Complete, No Changes Made"
summary_details+=("Would apply ${YELLOW}${total_applied:-0}${NC} optimizations")
summary_details+=("Run without ${YELLOW}--dry-run${NC} to apply these changes")
else
summary_title="Optimization and Check Complete"
# Build statistics summary
local -a stats=()
local cache_kb="${OPTIMIZE_CACHE_CLEANED_KB:-0}"
local db_count="${OPTIMIZE_DATABASES_COUNT:-0}"
local config_count="${OPTIMIZE_CONFIGS_REPAIRED:-0}"
if [[ "$cache_kb" =~ ^[0-9]+$ ]] && [[ "$cache_kb" -gt 0 ]]; then
local cache_human=$(bytes_to_human "$((cache_kb * 1024))")
stats+=("${cache_human} cache cleaned")
fi
if [[ "$db_count" =~ ^[0-9]+$ ]] && [[ "$db_count" -gt 0 ]]; then
stats+=("${db_count} databases optimized")
fi
if [[ "$config_count" =~ ^[0-9]+$ ]] && [[ "$config_count" -gt 0 ]]; then
stats+=("${config_count} configs repaired")
fi
# Build first summary line with most important stat only
local key_stat=""
if [[ "$cache_kb" =~ ^[0-9]+$ ]] && [[ "$cache_kb" -gt 0 ]]; then
local cache_human=$(bytes_to_human "$((cache_kb * 1024))")
key_stat="${cache_human} cache cleaned"
elif [[ "$db_count" =~ ^[0-9]+$ ]] && [[ "$db_count" -gt 0 ]]; then
key_stat="${db_count} databases optimized"
elif [[ "$config_count" =~ ^[0-9]+$ ]] && [[ "$config_count" -gt 0 ]]; then
key_stat="${config_count} configs repaired"
fi
if [[ -n "$key_stat" ]]; then
summary_details+=("Applied ${GREEN}${total_applied:-0}${NC} optimizations, ${key_stat}")
else
summary_details+=("Applied ${GREEN}${total_applied:-0}${NC} optimizations, all services tuned")
fi
local summary_line3=""
if [[ -n "${AUTO_FIX_SUMMARY:-}" ]]; then
summary_line3="${AUTO_FIX_SUMMARY}"
if [[ -n "${AUTO_FIX_DETAILS:-}" ]]; then
local detail_join
detail_join=$(echo "${AUTO_FIX_DETAILS}" | paste -sd ", " -)
[[ -n "$detail_join" ]] && summary_line3+=": ${detail_join}"
fi
summary_details+=("$summary_line3")
fi
summary_details+=("System fully optimized")
fi
print_summary_block "$summary_title" "${summary_details[@]}"
}
show_system_health() {
local health_json="$1"
local mem_used=$(echo "$health_json" | jq -r '.memory_used_gb // 0' 2> /dev/null || echo "0")
local mem_total=$(echo "$health_json" | jq -r '.memory_total_gb // 0' 2> /dev/null || echo "0")
local disk_used=$(echo "$health_json" | jq -r '.disk_used_gb // 0' 2> /dev/null || echo "0")
local disk_total=$(echo "$health_json" | jq -r '.disk_total_gb // 0' 2> /dev/null || echo "0")
local disk_percent=$(echo "$health_json" | jq -r '.disk_used_percent // 0' 2> /dev/null || echo "0")
local uptime=$(echo "$health_json" | jq -r '.uptime_days // 0' 2> /dev/null || echo "0")
mem_used=${mem_used:-0}
mem_total=${mem_total:-0}
disk_used=${disk_used:-0}
disk_total=${disk_total:-0}
disk_percent=${disk_percent:-0}
uptime=${uptime:-0}
printf "${ICON_ADMIN} System %.0f/%.0f GB RAM | %.0f/%.0f GB Disk | Uptime %.0fd\n" \
"$mem_used" "$mem_total" "$disk_used" "$disk_total" "$uptime"
}
parse_optimizations() {
local health_json="$1"
echo "$health_json" | jq -c '.optimizations[]' 2> /dev/null
}
announce_action() {
local name="$1"
local desc="$2"
local kind="$3"
if [[ "${FIRST_ACTION:-true}" == "true" ]]; then
export FIRST_ACTION=false
else
echo ""
fi
echo -e "${BLUE}${ICON_ARROW} ${name}${NC}"
}
touchid_configured() {
local pam_file="/etc/pam.d/sudo"
[[ -f "$pam_file" ]] && grep -q "pam_tid.so" "$pam_file" 2> /dev/null
}
touchid_supported() {
if command -v bioutil > /dev/null 2>&1; then
if bioutil -r 2> /dev/null | grep -qi "Touch ID"; then
return 0
fi
fi
# Fallback: Apple Silicon Macs usually have Touch ID.
if [[ "$(uname -m)" == "arm64" ]]; then
return 0
fi
return 1
}
cleanup_path() {
local raw_path="$1"
local label="$2"
local expanded_path="${raw_path/#\~/$HOME}"
if [[ ! -e "$expanded_path" ]]; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} $label"
return
fi
if should_protect_path "$expanded_path"; then
echo -e "${GRAY}${ICON_WARNING}${NC} Protected $label"
return
fi
local size_kb
size_kb=$(get_path_size_kb "$expanded_path")
local size_display=""
if [[ "$size_kb" =~ ^[0-9]+$ && "$size_kb" -gt 0 ]]; then
size_display=$(bytes_to_human "$((size_kb * 1024))")
fi
local removed=false
if safe_remove "$expanded_path" true; then
removed=true
elif request_sudo_access "Removing $label requires admin access"; then
if safe_sudo_remove "$expanded_path"; then
removed=true
fi
fi
if [[ "$removed" == "true" ]]; then
if [[ -n "$size_display" ]]; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} $label${NC}, ${GREEN}${size_display}${NC}"
else
echo -e "${GREEN}${ICON_SUCCESS}${NC} $label"
fi
else
echo -e "${GRAY}${ICON_WARNING}${NC} Skipped $label${GRAY}, grant Full Disk Access to your terminal and retry${NC}"
fi
}
ensure_directory() {
local raw_path="$1"
local expanded_path="${raw_path/#\~/$HOME}"
ensure_user_dir "$expanded_path"
}
declare -a SECURITY_FIXES=()
collect_security_fix_actions() {
SECURITY_FIXES=()
if [[ "${FIREWALL_DISABLED:-}" == "true" ]]; then
if ! is_whitelisted "firewall"; then
SECURITY_FIXES+=("firewall|Enable macOS firewall")
fi
fi
if [[ "${GATEKEEPER_DISABLED:-}" == "true" ]]; then
if ! is_whitelisted "gatekeeper"; then
SECURITY_FIXES+=("gatekeeper|Enable Gatekeeper, app download protection")
fi
fi
if touchid_supported && ! touchid_configured; then
if ! is_whitelisted "check_touchid"; then
SECURITY_FIXES+=("touchid|Enable Touch ID for sudo")
fi
fi
((${#SECURITY_FIXES[@]} > 0))
}
ask_for_security_fixes() {
if ! collect_security_fix_actions; then
return 1
fi
echo ""
echo -e "${BLUE}SECURITY FIXES${NC}"
for entry in "${SECURITY_FIXES[@]}"; do
IFS='|' read -r _ label <<< "$entry"
echo -e " ${ICON_LIST} $label"
done
echo ""
export MOLE_SECURITY_FIXES_SHOWN=true
echo -ne "${YELLOW}Apply now?${NC} ${GRAY}Enter confirm / Space cancel${NC}: "
local key
if ! key=$(read_key); then
export MOLE_SECURITY_FIXES_SKIPPED=true
echo -e "\n ${GRAY}${ICON_WARNING}${NC} Security fixes skipped"
echo ""
return 1
fi
if [[ "$key" == "ENTER" ]]; then
echo ""
return 0
else
export MOLE_SECURITY_FIXES_SKIPPED=true
echo -e "\n ${GRAY}${ICON_WARNING}${NC} Security fixes skipped"
echo ""
return 1
fi
}
apply_firewall_fix() {
if sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on > /dev/null 2>&1; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Firewall enabled"
FIREWALL_DISABLED=false
return 0
fi
echo -e " ${GRAY}${ICON_WARNING}${NC} Failed to enable firewall, check permissions"
return 1
}
apply_gatekeeper_fix() {
if sudo spctl --master-enable 2> /dev/null; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Gatekeeper enabled"
GATEKEEPER_DISABLED=false
return 0
fi
echo -e " ${GRAY}${ICON_WARNING}${NC} Failed to enable Gatekeeper"
return 1
}
apply_touchid_fix() {
if "$SCRIPT_DIR/bin/touchid.sh" enable; then
return 0
fi
return 1
}
perform_security_fixes() {
if ! ensure_sudo_session "Security changes require admin access"; then
echo -e "${GRAY}${ICON_WARNING}${NC} Skipped security fixes, sudo denied"
return 1
fi
local applied=0
for entry in "${SECURITY_FIXES[@]}"; do
IFS='|' read -r action _ <<< "$entry"
case "$action" in
firewall)
apply_firewall_fix && ((applied++))
;;
gatekeeper)
apply_gatekeeper_fix && ((applied++))
;;
touchid)
apply_touchid_fix && ((applied++))
;;
esac
done
if ((applied > 0)); then
log_success "Security settings updated"
fi
SECURITY_FIXES=()
}
cleanup_all() {
stop_inline_spinner 2> /dev/null || true
stop_sudo_session
cleanup_temp_files
# Log session end
log_operation_session_end "optimize" "${OPTIMIZE_SAFE_COUNT:-0}" "0"
}
handle_interrupt() {
cleanup_all
exit 130
}
main() {
# Set current command for operation logging
export MOLE_CURRENT_COMMAND="optimize"
local health_json
for arg in "$@"; do
case "$arg" in
"--debug")
export MO_DEBUG=1
;;
"--dry-run")
export MOLE_DRY_RUN=1
;;
"--whitelist")
manage_whitelist "optimize"
exit 0
;;
esac
done
log_operation_session_start "optimize"
trap cleanup_all EXIT
trap handle_interrupt INT TERM
if [[ -t 1 ]]; then
clear_screen
fi
print_header
# Dry-run indicator.
if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
echo -e "${YELLOW}${ICON_DRY_RUN} DRY RUN MODE${NC}, No files will be modified\n"
fi
if ! command -v jq > /dev/null 2>&1; then
echo -e "${YELLOW}${ICON_ERROR}${NC} Missing dependency: jq"
echo -e "${GRAY}Install with: ${GREEN}brew install jq${NC}"
exit 1
fi
if ! command -v bc > /dev/null 2>&1; then
echo -e "${YELLOW}${ICON_ERROR}${NC} Missing dependency: bc"
echo -e "${GRAY}Install with: ${GREEN}brew install bc${NC}"
exit 1
fi
if [[ -t 1 ]]; then
start_inline_spinner "Collecting system info..."
fi
if ! health_json=$(generate_health_json 2> /dev/null); then
if [[ -t 1 ]]; then
stop_inline_spinner
fi
echo ""
log_error "Failed to collect system health data"
exit 1
fi
if ! echo "$health_json" | jq empty 2> /dev/null; then
if [[ -t 1 ]]; then
stop_inline_spinner
fi
echo ""
log_error "Invalid system health data format"
echo -e "${YELLOW}Tip:${NC} Check if jq, awk, sysctl, and df commands are available"
exit 1
fi
if [[ -t 1 ]]; then
stop_inline_spinner
fi
show_system_health "$health_json"
load_whitelist "optimize"
if [[ ${#CURRENT_WHITELIST_PATTERNS[@]} -gt 0 ]]; then
local count=${#CURRENT_WHITELIST_PATTERNS[@]}
if [[ $count -le 3 ]]; then
local patterns_list=$(
IFS=', '
echo "${CURRENT_WHITELIST_PATTERNS[*]}"
)
echo -e "${ICON_ADMIN} Active Whitelist: ${patterns_list}"
fi
fi
local -a safe_items=()
local -a confirm_items=()
local opts_file
opts_file=$(mktemp_file)
parse_optimizations "$health_json" > "$opts_file"
while IFS= read -r opt_json; do
[[ -z "$opt_json" ]] && continue
local name=$(echo "$opt_json" | jq -r '.name')
local desc=$(echo "$opt_json" | jq -r '.description')
local action=$(echo "$opt_json" | jq -r '.action')
local path=$(echo "$opt_json" | jq -r '.path // ""')
local safe=$(echo "$opt_json" | jq -r '.safe')
local item="${name}|${desc}|${action}|${path}"
if [[ "$safe" == "true" ]]; then
safe_items+=("$item")
else
confirm_items+=("$item")
fi
done < "$opts_file"
echo ""
if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then
ensure_sudo_session "System optimization requires admin access" || true
fi
export FIRST_ACTION=true
if [[ ${#safe_items[@]} -gt 0 ]]; then
for item in "${safe_items[@]}"; do
IFS='|' read -r name desc action path <<< "$item"
announce_action "$name" "$desc" "safe"
execute_optimization "$action" "$path"
done
fi
if [[ ${#confirm_items[@]} -gt 0 ]]; then
for item in "${confirm_items[@]}"; do
IFS='|' read -r name desc action path <<< "$item"
announce_action "$name" "$desc" "confirm"
execute_optimization "$action" "$path"
done
fi
local safe_count=${#safe_items[@]}
local confirm_count=${#confirm_items[@]}
run_system_checks
export OPTIMIZE_SAFE_COUNT=$safe_count
export OPTIMIZE_CONFIRM_COUNT=$confirm_count
show_optimization_summary
printf '\n'
}
main "$@"