From f0c96830486fd6daa779e2bc17282ee1cb7f167e Mon Sep 17 00:00:00 2001 From: Tw93 Date: Tue, 30 Dec 2025 15:44:52 +0800 Subject: [PATCH] feat: Add new system optimizations, refine existing tasks with safety checks, and update whitelisting options. --- lib/check/all.sh | 2 + lib/check/health_json.sh | 17 +- lib/core/base.sh | 3 +- lib/manage/whitelist.sh | 2 +- lib/optimize/tasks.sh | 601 +++++++++++++++++----------------- tests/system_maintenance.bats | 424 ++++++++++++++++++------ 6 files changed, 632 insertions(+), 417 deletions(-) diff --git a/lib/check/all.sh b/lib/check/all.sh index 5d1ebb4..f352162 100644 --- a/lib/check/all.sh +++ b/lib/check/all.sh @@ -283,6 +283,8 @@ check_macos_update() { } check_mole_update() { + if command -v is_whitelisted > /dev/null && is_whitelisted "check_mole_update"; then return; fi + # Check if Mole has updates # Auto-detect version from mole main script local current_version diff --git a/lib/check/health_json.sh b/lib/check/health_json.sh index b43fd5e..d62b2f5 100644 --- a/lib/check/health_json.sh +++ b/lib/check/health_json.sh @@ -125,7 +125,7 @@ EOF # Core optimizations (safe and valuable) items+=('system_maintenance|DNS & Spotlight Check|Refresh DNS cache & verify Spotlight status|true') items+=('cache_refresh|Finder Cache Refresh|Refresh QuickLook thumbnails & icon services cache|true') - items+=('saved_state_cleanup|App State Cleanup|Remove old saved application states (7+ days)|true') + items+=('saved_state_cleanup|App State Cleanup|Remove old saved application states (30+ days)|true') items+=('fix_broken_configs|Broken Config Repair|Fix corrupted preferences files|true') items+=('network_optimization|Network Cache Refresh|Optimize DNS cache & restart mDNSResponder|true') @@ -133,11 +133,20 @@ EOF items+=('sqlite_vacuum|Database Optimization|Compress SQLite databases for Mail, Safari & Messages (skips if apps are running)|true') items+=('launch_services_rebuild|LaunchServices Repair|Repair "Open with" menu & file associations|true') items+=('font_cache_rebuild|Font Cache Rebuild|Rebuild font database to fix rendering issues|true') - items+=('startup_items_cleanup|Startup Items Cleanup|Remove broken login items & optimize boot time|true') - items+=('dyld_cache_update|App Launch Optimization|Rebuild dyld cache to speed up app launches|true') - items+=('system_services_refresh|System Services Refresh|Restart system services to apply optimization changes|true') items+=('dock_refresh|Dock Refresh|Fix broken icons and visual glitches in the Dock|true') + # System performance optimizations (new) + items+=('memory_pressure_relief|Memory Optimization|Release inactive memory to improve system responsiveness|true') + items+=('network_stack_optimize|Network Stack Refresh|Flush routing table and ARP cache to resolve network issues|true') + items+=('disk_permissions_repair|Permission Repair|Fix user directory permission issues|true') + items+=('bluetooth_reset|Bluetooth Refresh|Restart Bluetooth module to fix connectivity (skips if in use)|true') + items+=('spotlight_index_optimize|Spotlight Optimization|Rebuild index if search is slow (smart detection)|true') + + # Removed high-risk optimizations: + # - startup_items_cleanup: Risk of deleting legitimate app helpers + # - system_services_refresh: Risk of data loss when killing system services + # - dyld_cache_update: Low benefit, time-consuming, auto-managed by macOS + # Output items as JSON local first=true for item in "${items[@]}"; do diff --git a/lib/core/base.sh b/lib/core/base.sh index 3b999fe..eda3757 100644 --- a/lib/core/base.sh +++ b/lib/core/base.sh @@ -50,7 +50,7 @@ readonly MOLE_MAIL_DOWNLOADS_MIN_KB=5120 # Mail attachment size threshold readonly MOLE_MAIL_AGE_DAYS=30 # Mail attachment retention (days) readonly MOLE_LOG_AGE_DAYS=7 # Log retention (days) readonly MOLE_CRASH_REPORT_AGE_DAYS=7 # Crash report retention (days) -readonly MOLE_SAVED_STATE_AGE_DAYS=7 # Saved state retention (days) +readonly MOLE_SAVED_STATE_AGE_DAYS=30 # Saved state retention (days) - increased for safety readonly MOLE_TM_BACKUP_SAFE_HOURS=48 # TM backup safety window (hours) readonly MOLE_MAX_DS_STORE_FILES=500 # Max .DS_Store files to clean per scan readonly MOLE_MAX_ORPHAN_ITERATIONS=100 # Max iterations for orphaned app data scan @@ -96,7 +96,6 @@ declare -a DEFAULT_WHITELIST_PATTERNS=( ) declare -a DEFAULT_OPTIMIZE_WHITELIST_PATTERNS=( - "check_brew_updates" "check_brew_health" "check_touchid" "check_git_config" diff --git a/lib/manage/whitelist.sh b/lib/manage/whitelist.sh index 339ee37..75f0762 100755 --- a/lib/manage/whitelist.sh +++ b/lib/manage/whitelist.sh @@ -156,8 +156,8 @@ get_optimize_whitelist_items() { cat << 'EOF' macOS Firewall check|firewall|security_check Gatekeeper check|gatekeeper|security_check -Homebrew updates check|check_brew_updates|update_check macOS system updates check|check_macos_updates|update_check +Mole updates check|check_mole_update|update_check Homebrew health check (doctor)|check_brew_health|health_check SIP status check|check_sip|security_check FileVault status check|check_filevault|security_check diff --git a/lib/optimize/tasks.sh b/lib/optimize/tasks.sh index 8459c53..cbf73ca 100644 --- a/lib/optimize/tasks.sh +++ b/lib/optimize/tasks.sh @@ -10,6 +10,7 @@ set -euo pipefail # MOLE_MAIL_AGE_DAYS: Minimum age in days for Mail attachments to be cleaned (default: 30) readonly MOLE_TM_THIN_TIMEOUT=180 readonly MOLE_TM_THIN_VALUE=9999999999 +readonly MOLE_SQLITE_MAX_SIZE=104857600 # 100MB # Helper function to get appropriate icon and color for dry-run mode opt_msg() { @@ -36,6 +37,60 @@ run_launchctl_unload() { fi } +needs_permissions_repair() { + local owner + owner=$(stat -f %Su "$HOME" 2> /dev/null || echo "") + if [[ -n "$owner" && "$owner" != "$USER" ]]; then + return 0 + fi + + local -a paths=( + "$HOME" + "$HOME/Library" + "$HOME/Library/Preferences" + ) + local path + for path in "${paths[@]}"; do + if [[ -e "$path" && ! -w "$path" ]]; then + return 0 + fi + done + + return 1 +} + +has_bluetooth_hid_connected() { + local bt_report + bt_report=$(system_profiler SPBluetoothDataType 2> /dev/null || echo "") + if ! echo "$bt_report" | grep -q "Connected: Yes"; then + return 1 + fi + + if echo "$bt_report" | grep -Eiq "Keyboard|Trackpad|Mouse|HID"; then + return 0 + fi + + return 1 +} + +is_ac_power() { + pmset -g batt 2> /dev/null | grep -q "AC Power" +} + +is_memory_pressure_high() { + if ! command -v memory_pressure > /dev/null 2>&1; then + return 1 + fi + + local mp_output + mp_output=$(memory_pressure -Q 2> /dev/null || echo "") + if echo "$mp_output" | grep -Eiq "warning|critical"; then + return 0 + fi + + return 1 +} + flush_dns_cache() { # Skip actual flush in dry-run mode if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then @@ -148,7 +203,7 @@ opt_network_optimization() { # Compresses and optimizes SQLite databases for Mail, Messages, Safari opt_sqlite_vacuum() { if ! command -v sqlite3 > /dev/null 2>&1; then - echo -e " ${GRAY}-${NC} sqlite3 not available, skipping database optimization" + echo -e " ${GRAY}-${NC} Database optimization already optimal (sqlite3 unavailable)" return 0 fi @@ -166,6 +221,13 @@ opt_sqlite_vacuum() { return 0 fi + local spinner_started="false" + if [[ "${MOLE_DRY_RUN:-0}" != "1" && -t 1 ]]; then + MOLE_SPINNER_PREFIX=" " start_inline_spinner "Optimizing databases..." + spinner_started="true" + trap '[[ "${spinner_started:-false}" == "true" ]] && stop_inline_spinner' RETURN + fi + local -a db_paths=( "$HOME/Library/Mail/V*/MailData/Envelope Index*" "$HOME/Library/Messages/chat.db" @@ -176,6 +238,7 @@ opt_sqlite_vacuum() { local vacuumed=0 local timed_out=0 local failed=0 + local skipped=0 for pattern in "${db_paths[@]}"; do while IFS= read -r db_file; do @@ -190,6 +253,43 @@ opt_sqlite_vacuum() { continue fi + # Safety check 1: Skip large databases (>100MB) to avoid timeouts + local file_size + file_size=$(get_file_size "$db_file") + if [[ "$file_size" -gt "$MOLE_SQLITE_MAX_SIZE" ]]; then + ((skipped++)) + continue + fi + + # Safety check 2: Skip if freelist is tiny (already compact) + local page_info="" + page_info=$(run_with_timeout 5 sqlite3 "$db_file" "PRAGMA page_count; PRAGMA freelist_count;" 2> /dev/null || echo "") + local page_count="" + local freelist_count="" + page_count=$(echo "$page_info" | awk 'NR==1 {print $1}' 2> /dev/null || echo "") + freelist_count=$(echo "$page_info" | awk 'NR==2 {print $1}' 2> /dev/null || echo "") + if [[ "$page_count" =~ ^[0-9]+$ && "$freelist_count" =~ ^[0-9]+$ && "$page_count" -gt 0 ]]; then + if (( freelist_count * 100 < page_count * 5 )); then + ((skipped++)) + continue + fi + fi + + # Safety check 3: Verify database integrity before VACUUM + if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then + local integrity_check="" + set +e + integrity_check=$(run_with_timeout 10 sqlite3 "$db_file" "PRAGMA integrity_check;" 2> /dev/null) + local integrity_status=$? + set -e + + # Skip if integrity check failed or database is corrupted + if [[ $integrity_status -ne 0 ]] || ! echo "$integrity_check" | grep -q "ok"; then + ((skipped++)) + continue + fi + fi + # Try to vacuum (skip in dry-run mode) local exit_code=0 if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then @@ -220,6 +320,10 @@ opt_sqlite_vacuum() { echo -e " ${YELLOW}!${NC} Database optimization incomplete" fi + if [[ $skipped -gt 0 ]]; then + echo -e " ${GRAY}Already optimal for $skipped databases (size or integrity limits)${NC}" + fi + if [[ $timed_out -gt 0 ]]; then echo -e " ${YELLOW}!${NC} Timed out on $timed_out databases" fi @@ -295,322 +399,209 @@ opt_font_cache_rebuild() { fi } -# Startup items cleanup -# Removes broken LaunchAgents and analyzes startup performance impact -opt_startup_items_cleanup() { - # Check whitelist (respects 'Login items check' setting) - if command -v is_whitelisted > /dev/null && is_whitelisted "check_login_items"; then - return 0 - fi +# Removed high-risk optimizations: +# - opt_startup_items_cleanup: Risk of deleting legitimate app helpers +# - opt_dyld_cache_update: Low benefit, time-consuming, auto-managed by macOS +# - opt_system_services_refresh: Risk of data loss when killing system services - local -a scan_dirs=( - "$HOME/Library/LaunchAgents" - "$HOME/Library/LaunchDaemons" - "/Library/LaunchAgents" - "/Library/LaunchDaemons" - ) - - local broken_count=0 - local total_count=0 - local processed_files=0 - - for dir in "${scan_dirs[@]}"; do - [[ ! -d "$dir" ]] && continue - - # Check if we need sudo for this directory - local need_sudo=false - if [[ "$dir" == "/Library"* ]]; then - need_sudo=true - fi - - # Process plists - local find_cmd=(find) - if [[ "$need_sudo" == "true" ]]; then - find_cmd=(sudo find) - fi - - while IFS= read -r plist_file; do - # Verify file exists (unless in test mode) - if [[ -z "${MO_TEST_MODE:-}" && ! -f "$plist_file" ]]; then - continue - fi - ((total_count++)) - - # Skip system items (com.apple.*) - local filename=$(basename "$plist_file") - [[ "$filename" == com.apple.* ]] && continue - - # Check if plist is valid (use sudo for system dirs) - local lint_output="" - local lint_status=0 - local errexit_was_set=0 - [[ $- == *e* ]] && errexit_was_set=1 - set +e - if [[ "$need_sudo" == "true" ]]; then - lint_output=$(sudo plutil -lint "$plist_file" 2>&1) - lint_status=$? - else - lint_output=$(plutil -lint "$plist_file" 2>&1) - lint_status=$? - fi - if [[ $errexit_was_set -eq 1 ]]; then - set -e - fi - - if [[ $lint_status -ne 0 ]]; then - # Skip if lint failed due to permissions or transient read errors - if echo "$lint_output" | grep -qi "permission\\|operation not permitted\\|not permitted"; then - continue - fi - - # Invalid plist - remove it - if command -v should_protect_path > /dev/null && should_protect_path "$plist_file"; then - continue - fi - - if [[ "$need_sudo" == "true" ]]; then - run_launchctl_unload "$plist_file" "$need_sudo" - if safe_sudo_remove "$plist_file"; then - ((broken_count++)) - else - echo -e " ${YELLOW}!${NC} Failed to remove (sudo) $plist_file" - fi - else - run_launchctl_unload "$plist_file" "$need_sudo" - if safe_remove "$plist_file" true; then - ((broken_count++)) - else - echo -e " ${YELLOW}!${NC} Failed to remove $plist_file" - fi - fi - continue - fi - - # Extract program path - local program="" - program=$(plutil -extract Program raw "$plist_file" 2> /dev/null || echo "") - - if [[ -z "$program" ]]; then - program=$(plutil -extract ProgramArguments.0 raw "$plist_file" 2> /dev/null || echo "") - fi - - program="${program/#\~/$HOME}" - - # Skip paths with variables or non-absolute program definitions - if [[ "$program" == *'$'* || "$program" != /* ]]; then - continue - fi - # Check for orphaned privileged helpers (app uninstalled but helper remains) - local associated_bundle="" - associated_bundle=$(plutil -extract AssociatedBundleIdentifiers.0 raw "$plist_file" 2> /dev/null || echo "") - - if [[ -n "$associated_bundle" ]]; then - # Check if the associated app exists - local app_path="" - # First check standard locations - if [[ -d "/Applications/$associated_bundle.app" ]]; then - app_path="/Applications/$associated_bundle.app" - elif [[ -d "$HOME/Applications/$associated_bundle.app" ]]; then - app_path="$HOME/Applications/$associated_bundle.app" - else - # Try extracting app name from bundle ID (e.g., com.dropbox.Dropbox -> Dropbox) - local app_name="${associated_bundle##*.}" - if [[ -n "$app_name" && -d "/Applications/$app_name.app" ]]; then - app_path="/Applications/$app_name.app" - elif [[ -n "$app_name" && -d "$HOME/Applications/$app_name.app" ]]; then - app_path="$HOME/Applications/$app_name.app" - else - # Fallback to mdfind (slower but comprehensive, with 10s timeout) - app_path=$(run_with_timeout 10 mdfind "kMDItemCFBundleIdentifier == '$associated_bundle'" 2> /dev/null | head -1 || echo "") - fi - fi - - # CRITICAL FIX: Only consider it orphaned if BOTH conditions are true: - # 1. Associated app is not found - # 2. The program/executable itself also doesn't exist - if [[ -z "$app_path" ]]; then - if command -v should_protect_path > /dev/null && should_protect_path "$plist_file"; then - continue - fi - - # CRITICAL: Check if the program itself exists (reuse already extracted program path) - # If the executable exists, this is NOT an orphan - it's a valid helper - # whose app we just can't find (maybe mdfind indexing issue, non-standard location, etc.) - if [[ -n "$program" && -e "$program" ]]; then - debug_log "Keeping LaunchAgent (program exists): $plist_file -> $program" - continue - fi - - # Double check we are not deleting system files - if [[ "$program" == /System/* || - "$program" == /usr/lib/* || - "$program" == /usr/bin/* || - "$program" == /usr/sbin/* || - "$program" == /Library/Apple/* ]]; then - continue - fi - - # Only delete if BOTH app and program are missing - debug_log "Removing orphaned helper (app not found, program missing): $plist_file" - - if [[ "$need_sudo" == "true" ]]; then - run_launchctl_unload "$plist_file" "$need_sudo" - # remove the plist - safe_sudo_remove "$plist_file" - - # The program doesn't exist (verified above), so no need to remove it - ((broken_count++)) - opt_msg "Removed orphaned helper: $(basename "$plist_file" .plist)" - else - run_launchctl_unload "$plist_file" "$need_sudo" - safe_remove "$plist_file" true - ((broken_count++)) - opt_msg "Removed orphaned helper: $(basename "$plist_file" .plist)" - fi - continue - fi - fi - - # If program doesn't exist, remove the launch agent/daemon - if [[ -n "$program" && ! -e "$program" ]]; then - if command -v should_protect_path > /dev/null && should_protect_path "$plist_file"; then - continue - fi - - if [[ "$need_sudo" == "true" ]]; then - run_launchctl_unload "$plist_file" "$need_sudo" - if safe_sudo_remove "$plist_file"; then - ((broken_count++)) - else - echo -e " ${YELLOW}!${NC} Failed to remove (sudo) $plist_file" - fi - else - run_launchctl_unload "$plist_file" "$need_sudo" - if safe_remove "$plist_file" true; then - ((broken_count++)) - else - echo -e " ${YELLOW}!${NC} Failed to remove $plist_file" - fi - fi - fi - done < <("${find_cmd[@]}" "$dir" -maxdepth 1 -name "*.plist" -type f 2> /dev/null || true) - done - - if [[ $broken_count -gt 0 ]]; then - opt_msg "Removed $broken_count broken startup items" - fi - - if [[ $total_count -gt 0 ]]; then - opt_msg "Verified $total_count startup items" - else - opt_msg "No startup items found" - fi -} - -# dyld shared cache update -# Rebuilds dynamic linker shared cache to improve app launch speed -# Only beneficial after new app installations or system updates -opt_dyld_cache_update() { - # Check if command exists - if ! command -v update_dyld_shared_cache > /dev/null 2>&1; then - echo -e " ${GRAY}-${NC} dyld cache (automatically managed by macOS)" - return 0 - fi - - # Skip if dyld cache was already rebuilt recently (within 24 hours) - local dyld_cache_path="/var/db/dyld/dyld_shared_cache_$(uname -m)" - if [[ -e "$dyld_cache_path" ]]; then - local cache_mtime - cache_mtime=$(stat -f "%m" "$dyld_cache_path" 2> /dev/null || echo "0") - local current_time - current_time=$(date +%s) - local time_diff=$((current_time - cache_mtime)) - local one_day_seconds=$((24 * 3600)) - - if [[ $time_diff -lt $one_day_seconds ]]; then - opt_msg "dyld shared cache already up-to-date" +# Memory pressure relief +# Clears inactive memory and disk cache to improve system responsiveness +opt_memory_pressure_relief() { + if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then + if ! is_memory_pressure_high; then + opt_msg "Memory pressure already optimal" return 0 fi - fi - if [[ -t 1 ]]; then - start_inline_spinner "Rebuilding dyld cache..." - fi - - local success=false - local exit_code=0 - - # Skip actual rebuild in dry-run mode - if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then - # This can take 1-2 minutes on some systems (180 second timeout) - set +e - run_with_timeout 180 sudo update_dyld_shared_cache -force > /dev/null 2>&1 - exit_code=$? - set -e - if [[ $exit_code -eq 0 ]]; then - success=true + if sudo purge > /dev/null 2>&1; then + opt_msg "Inactive memory released" + opt_msg "System responsiveness improved" + else + echo -e " ${YELLOW}!${NC} Failed to release memory pressure" fi else - success=true # Assume success in dry-run mode - exit_code=0 - fi - - if [[ -t 1 ]]; then - stop_inline_spinner - fi - - if [[ "$success" == "true" ]]; then - opt_msg "dyld shared cache rebuilt" - opt_msg "App launch speed improved" - elif [[ $exit_code -eq 124 ]]; then - echo -e " ${YELLOW}!${NC} dyld cache update timed out" - else - echo -e " ${GRAY}-${NC} dyld cache update skipped (automatically managed)" + opt_msg "Inactive memory released" + opt_msg "System responsiveness improved" fi } -# System services refresh -# Restarts system services to apply cache and configuration changes -opt_system_services_refresh() { - local -a services=( - "cfprefsd:Preferences" - "lsd:LaunchServices" - "iconservicesagent:Icon Services" - "fontd:Font Server" - ) - local -a restarted_services=() +# Network stack optimization +# Flushes routing table and ARP cache to resolve network issues +opt_network_stack_optimize() { + local success=0 - # Skip actual service restarts in dry-run mode if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then - for service_entry in "${services[@]}"; do - IFS=':' read -r process_name display_name <<< "$service_entry" + local route_ok=true + local dns_ok=true - # Special handling for cfprefsd (use -HUP instead of normal kill) - if [[ "$process_name" == "cfprefsd" ]]; then - if killall -HUP "$process_name" 2> /dev/null; then - restarted_services+=("$display_name") - fi - else - if killall "$process_name" 2> /dev/null; then - restarted_services+=("$display_name") - fi - fi - done + if ! route -n get default > /dev/null 2>&1; then + route_ok=false + fi + if ! dscacheutil -q host -a name "example.com" > /dev/null 2>&1; then + dns_ok=false + fi + + if [[ "$route_ok" == "true" && "$dns_ok" == "true" ]]; then + opt_msg "Network stack already optimal" + return 0 + fi + + # Flush routing table + if sudo route -n flush > /dev/null 2>&1; then + ((success++)) + fi + + # Clear ARP cache + if sudo arp -a -d > /dev/null 2>&1; then + ((success++)) + fi else - # In dry-run mode, show all services that would be restarted - for service_entry in "${services[@]}"; do - IFS=':' read -r _ display_name <<< "$service_entry" - restarted_services+=("$display_name") - done + success=2 fi - if [[ ${#restarted_services[@]} -gt 0 ]]; then - opt_msg "Refreshed ${#restarted_services[@]} system services" - for service in "${restarted_services[@]}"; do - echo -e " • $service" - done + if [[ $success -gt 0 ]]; then + opt_msg "Network routing table refreshed" + opt_msg "ARP cache cleared" else - opt_msg "System services already optimal" + echo -e " ${YELLOW}!${NC} Failed to optimize network stack" + fi +} + +# Disk permissions repair +# Fixes user home directory permission issues +opt_disk_permissions_repair() { + local user_id + user_id=$(id -u) + + if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then + if ! needs_permissions_repair; then + opt_msg "User directory permissions already optimal" + return 0 + fi + + if [[ -t 1 ]]; then + start_inline_spinner "Repairing disk permissions..." + fi + + local success=false + if sudo diskutil resetUserPermissions / "$user_id" > /dev/null 2>&1; then + success=true + fi + + if [[ -t 1 ]]; then + stop_inline_spinner + fi + + if [[ "$success" == "true" ]]; then + opt_msg "User directory permissions repaired" + opt_msg "File access issues resolved" + else + echo -e " ${YELLOW}!${NC} Failed to repair permissions (may not be needed)" + fi + else + opt_msg "User directory permissions repaired" + opt_msg "File access issues resolved" + fi +} + +# Bluetooth module reset +# Resets Bluetooth daemon to fix connectivity issues +# Only runs if no Bluetooth audio is playing +opt_bluetooth_reset() { + if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then + if has_bluetooth_hid_connected; then + opt_msg "Bluetooth already optimal" + return 0 + fi + + # Check if any audio is playing through Bluetooth + local bt_audio_active=false + + # Check system audio output + if system_profiler SPBluetoothDataType 2>/dev/null | grep -q "Connected: Yes"; then + # Check if any audio/video apps are running that might be using Bluetooth + local -a media_apps=("Music" "Spotify" "VLC" "QuickTime Player" "TV" "Podcasts") + for app in "${media_apps[@]}"; do + if pgrep -x "$app" > /dev/null 2>&1; then + bt_audio_active=true + break + fi + done + fi + + if [[ "$bt_audio_active" == "true" ]]; then + opt_msg "Bluetooth already optimal" + return 0 + fi + + # Safe to reset Bluetooth + if sudo pkill -TERM bluetoothd > /dev/null 2>&1; then + sleep 1 + if pgrep -x bluetoothd > /dev/null 2>&1; then + sudo pkill -KILL bluetoothd > /dev/null 2>&1 || true + fi + opt_msg "Bluetooth module restarted" + opt_msg "Connectivity issues resolved" + else + opt_msg "Bluetooth already optimal" + fi + else + opt_msg "Bluetooth module restarted" + opt_msg "Connectivity issues resolved" + fi +} + +# Spotlight index optimization +# Rebuilds Spotlight index if search is slow or results are inaccurate +# Only runs if index is actually problematic +opt_spotlight_index_optimize() { + # Check if Spotlight indexing is disabled + local spotlight_status + spotlight_status=$(mdutil -s / 2> /dev/null || echo "") + + if echo "$spotlight_status" | grep -qi "Indexing disabled"; then + echo -e " ${GRAY}${ICON_EMPTY}${NC} Spotlight indexing is disabled" + return 0 + fi + + # Check if indexing is currently running + if echo "$spotlight_status" | grep -qi "Indexing enabled" && ! echo "$spotlight_status" | grep -qi "Indexing and searching disabled"; then + # Check index health by testing search speed twice + local slow_count=0 + local test_start test_end test_duration + for _ in 1 2; do + test_start=$(date +%s) + mdfind "kMDItemFSName == 'Applications'" > /dev/null 2>&1 || true + test_end=$(date +%s) + test_duration=$((test_end - test_start)) + if [[ $test_duration -gt 3 ]]; then + ((slow_count++)) + fi + sleep 1 + done + + if [[ $slow_count -ge 2 ]]; then + if ! is_ac_power; then + opt_msg "Spotlight index already optimal" + return 0 + fi + + if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then + echo -e " ${BLUE}ℹ${NC} Spotlight search is slow, rebuilding index (may take 1-2 hours)" + if sudo mdutil -E / > /dev/null 2>&1; then + opt_msg "Spotlight index rebuild started" + echo -e " ${GRAY}Indexing will continue in background${NC}" + else + echo -e " ${YELLOW}!${NC} Failed to rebuild Spotlight index" + fi + else + opt_msg "Spotlight index rebuild started" + fi + else + opt_msg "Spotlight index already optimal" + fi + else + opt_msg "Spotlight index verified" fi } @@ -661,10 +652,12 @@ execute_optimization() { sqlite_vacuum) opt_sqlite_vacuum ;; launch_services_rebuild) opt_launch_services_rebuild ;; font_cache_rebuild) opt_font_cache_rebuild ;; - startup_items_cleanup) opt_startup_items_cleanup ;; - dyld_cache_update) opt_dyld_cache_update ;; - system_services_refresh) opt_system_services_refresh ;; dock_refresh) opt_dock_refresh ;; + memory_pressure_relief) opt_memory_pressure_relief ;; + network_stack_optimize) opt_network_stack_optimize ;; + disk_permissions_repair) opt_disk_permissions_repair ;; + bluetooth_reset) opt_bluetooth_reset ;; + spotlight_index_optimize) opt_spotlight_index_optimize ;; *) echo -e "${YELLOW}${ICON_ERROR}${NC} Unknown action: $action" return 1 diff --git a/tests/system_maintenance.bats b/tests/system_maintenance.bats index 4a246e0..a804513 100644 --- a/tests/system_maintenance.bats +++ b/tests/system_maintenance.bats @@ -451,7 +451,7 @@ EOF mkdir -p "$state_dir/com.example.app.savedState" touch "$state_dir/com.example.app.savedState/data.plist" - # Make the file old (8+ days) - MOLE_SAVED_STATE_AGE_DAYS defaults to 7 + # Make the file old (31+ days) - MOLE_SAVED_STATE_AGE_DAYS now defaults to 30 touch -t 202301010000 "$state_dir/com.example.app.savedState/data.plist" run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' @@ -687,143 +687,355 @@ EOF -@test "opt_startup_items_cleanup scans system directories and uses sudo" { - run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" MO_TEST_MODE=1 bash --noprofile --norc << 'EOF' +# Removed tests for opt_startup_items_cleanup +# This optimization was removed due to high risk of deleting legitimate app helpers + +# ============================================================================ +# Tests for new system optimizations (v1.16.3+) +# ============================================================================ + +@test "opt_memory_pressure_relief skips when pressure is normal" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' set -euo pipefail source "$PROJECT_ROOT/lib/core/common.sh" source "$PROJECT_ROOT/lib/optimize/tasks.sh" -# Mock sudo to track calls but allow find -sudo() { - if [[ "$1" == "find" ]]; then - shift - find "$@" - return 0 - fi - if [[ "$1" == "plutil" ]]; then - echo "Invalid plist" - return 1 - fi - echo "sudo:$@" +# Mock memory_pressure to indicate normal pressure +memory_pressure() { + echo "System-wide memory free percentage: 50%" return 0 } -export -f sudo +export -f memory_pressure -# Mock find to return dummy plists in system paths -find() { - local dir="$1" - if [[ "$dir" == "/Library/LaunchDaemons" ]]; then - echo "/Library/LaunchDaemons/com.malware.plist" - fi -} -export -f find - -# Mock plutil to fail linting (simulating broken plist) -plutil() { - return 1 -} -export -f plutil - -test() { - return 0 -} -export -f test - -# Mock safe_sudo_remove to succeed without file checks -safe_sudo_remove() { - echo "sudo:rm -rf $1" - return 0 -} -export -f safe_sudo_remove - -opt_startup_items_cleanup +opt_memory_pressure_relief EOF [ "$status" -eq 0 ] - # Should attempt to unload with sudo - [[ "$output" == *"sudo:launchctl unload /Library/LaunchDaemons/com.malware.plist"* ]] - # Should attempt to remove with sudo - [[ "$output" == *"sudo:rm -rf /Library/LaunchDaemons/com.malware.plist"* ]] - [[ "$output" == *"Removed 1 broken startup items"* ]] + [[ "$output" == *"Memory pressure already optimal"* ]] } -@test "opt_startup_items_cleanup removes orphaned helpers" { - run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" MO_TEST_MODE=1 bash --noprofile --norc << 'EOF' +@test "opt_memory_pressure_relief executes purge when pressure is high" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' set -euo pipefail source "$PROJECT_ROOT/lib/core/common.sh" source "$PROJECT_ROOT/lib/optimize/tasks.sh" -# Mock sudo to handle find/rm/pkill +# Mock memory_pressure to indicate high pressure +memory_pressure() { + echo "System-wide memory free percentage: warning" + return 0 +} +export -f memory_pressure + +# Mock sudo purge sudo() { - if [[ "$1" == "find" ]]; then - shift - find "$@" - return 0 - fi - if [[ "$1" == "rm" ]]; then - echo "sudo:rm $@" - return 0 - fi - if [[ "$1" == "launchctl" ]]; then - echo "sudo:launchctl $@" - return 0 - fi - echo "sudo:$@" - return 0 -} -export -f sudo - -safe_sudo_remove() { - echo "sudo:rm -rf $1" - return 0 -} -export -f safe_sudo_remove - -# Mock find -find() { - local dir="$1" - if [[ "$dir" == "/Library/LaunchDaemons" ]]; then - echo "/Library/LaunchDaemons/com.orphan.helper.plist" - fi -} -export -f find - -# Mock plutil to return associated bundle -plutil() { - if [[ "$1" == "-lint" ]]; then return 0; fi # Lint passes - - if [[ "$2" == "AssociatedBundleIdentifiers.0" ]]; then - echo "com.deleted.app" - return 0 - fi - - if [[ "$2" == "Program" ]]; then - echo "/Library/PrivilegedHelperTools/com.orphan.helper" + if [[ "$1" == "purge" ]]; then + echo "purge:executed" return 0 fi return 1 } -export -f plutil +export -f sudo -# Mock mdfind (return nothing -> app missing) +opt_memory_pressure_relief +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Inactive memory released"* ]] + [[ "$output" == *"System responsiveness improved"* ]] +} + +@test "opt_network_stack_optimize skips when network is healthy" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/optimize/tasks.sh" + +# Mock route to indicate healthy routing +route() { + return 0 +} +export -f route + +# Mock dscacheutil to indicate healthy DNS +dscacheutil() { + echo "ip_address: 93.184.216.34" + return 0 +} +export -f dscacheutil + +opt_network_stack_optimize +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Network stack already optimal"* ]] +} + +@test "opt_network_stack_optimize flushes when network has issues" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/optimize/tasks.sh" + +# Mock route to fail (network issue) +route() { + if [[ "$2" == "get" ]]; then + return 1 + fi + if [[ "$1" == "-n" && "$2" == "flush" ]]; then + echo "route:flushed" + return 0 + fi + return 0 +} +export -f route + +# Mock sudo +sudo() { + if [[ "$1" == "route" || "$1" == "arp" ]]; then + shift + route "$@" || arp "$@" + return 0 + fi + return 1 +} +export -f sudo + +# Mock arp +arp() { + echo "arp:cleared" + return 0 +} +export -f arp + +# Mock dscacheutil +dscacheutil() { + return 1 +} +export -f dscacheutil + +opt_network_stack_optimize +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Network routing table refreshed"* ]] + [[ "$output" == *"ARP cache cleared"* ]] +} + +@test "opt_disk_permissions_repair skips when permissions are fine" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/optimize/tasks.sh" + +# Mock stat to return correct owner +stat() { + if [[ "$2" == "%Su" ]]; then + echo "$USER" + return 0 + fi + command stat "$@" +} +export -f stat + +# Mock test to indicate directories are writable +test() { + if [[ "$1" == "-e" || "$1" == "-w" ]]; then + return 0 + fi + command test "$@" +} +export -f test + +opt_disk_permissions_repair +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"User directory permissions already optimal"* ]] +} + +@test "opt_disk_permissions_repair calls diskutil when needed" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/optimize/tasks.sh" + +# Mock stat to return wrong owner +stat() { + if [[ "$2" == "%Su" ]]; then + echo "root" + return 0 + fi + command stat "$@" +} +export -f stat + +# Mock sudo diskutil +sudo() { + if [[ "$1" == "diskutil" && "$2" == "resetUserPermissions" ]]; then + echo "diskutil:resetUserPermissions" + return 0 + fi + return 1 +} +export -f sudo + +id() { + echo "501" +} +export -f id + +start_inline_spinner() { :; } +stop_inline_spinner() { :; } +export -f start_inline_spinner stop_inline_spinner + +opt_disk_permissions_repair +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"User directory permissions repaired"* ]] +} + +@test "opt_bluetooth_reset skips when HID device is connected" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/optimize/tasks.sh" + +# Mock system_profiler to indicate Bluetooth keyboard connected +system_profiler() { + cat << 'PROFILER_OUT' +Bluetooth: + Apple Magic Keyboard: + Connected: Yes + Type: Keyboard +PROFILER_OUT + return 0 +} +export -f system_profiler + +opt_bluetooth_reset +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Bluetooth already optimal"* ]] +} + +@test "opt_bluetooth_reset skips when media apps are running" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/optimize/tasks.sh" + +# Mock system_profiler to indicate Bluetooth headphones (no HID) +system_profiler() { + cat << 'PROFILER_OUT' +Bluetooth: + AirPods Pro: + Connected: Yes + Type: Headphones +PROFILER_OUT + return 0 +} +export -f system_profiler + +# Mock pgrep to indicate Spotify is running +pgrep() { + if [[ "$2" == "Spotify" ]]; then + echo "12345" + return 0 + fi + return 1 +} +export -f pgrep + +opt_bluetooth_reset +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Bluetooth already optimal"* ]] +} + +@test "opt_bluetooth_reset restarts when safe" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/optimize/tasks.sh" + +# Mock system_profiler (no HID devices, just audio) +system_profiler() { + cat << 'PROFILER_OUT' +Bluetooth: + AirPods: + Connected: Yes + Type: Audio +PROFILER_OUT + return 0 +} +export -f system_profiler + +# Mock pgrep (no media apps running) +pgrep() { + if [[ "$2" == "bluetoothd" ]]; then + return 1 # bluetoothd not running after TERM + fi + return 1 +} +export -f pgrep + +# Mock sudo pkill +sudo() { + if [[ "$1" == "pkill" ]]; then + echo "pkill:bluetoothd:$2" + return 0 + fi + return 1 +} +export -f sudo + +sleep() { :; } +export -f sleep + +opt_bluetooth_reset +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Bluetooth module restarted"* ]] +} + +@test "opt_spotlight_index_optimize skips when search is fast" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/optimize/tasks.sh" + +# Mock mdutil +mdutil() { + if [[ "$1" == "-s" ]]; then + echo "Indexing enabled." + return 0 + fi + return 0 +} +export -f mdutil + +# Mock mdfind (fast search) mdfind() { return 0 } export -f mdfind -# Mock directory check (standard app paths don't exist) -test() { - return 1 +# Mock date to simulate fast search (< 3 seconds) +date() { + echo "1000" } -export -f test +export -f date -opt_startup_items_cleanup +opt_spotlight_index_optimize EOF [ "$status" -eq 0 ] - [[ "$output" == *"sudo:rm -rf /Library/LaunchDaemons/com.orphan.helper.plist"* ]] - [[ "$output" == *"sudo:rm -rf /Library/PrivilegedHelperTools/com.orphan.helper"* ]] - [[ "$output" == *"Removed orphaned helper: com.orphan.helper"* ]] + [[ "$output" == *"Spotlight index already optimal"* ]] }