mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 15:04:42 +00:00
- Fix safe_remove set -e trap in command substitution - Fix has_full_disk_access false positives and unknown state handling - Use set +e in perform_cleanup for graceful degradation - Track removal failures and only count actually deleted items (#180) - Add "Skipped X items (permission denied or in use)" notification - Improve spinner reliability with cooperative stop mechanism (#175)
This commit is contained in:
@@ -1,35 +1,28 @@
|
||||
#!/bin/bash
|
||||
# System-Level Cleanup Module
|
||||
# Deep system cleanup (requires sudo) and Time Machine failed backups
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Deep system cleanup (requires sudo)
|
||||
clean_deep_system() {
|
||||
stop_section_spinner
|
||||
|
||||
# Clean old system caches
|
||||
local cache_cleaned=0
|
||||
safe_sudo_find_delete "/Library/Caches" "*.cache" "$MOLE_TEMP_FILE_AGE_DAYS" "f" && cache_cleaned=1 || true
|
||||
safe_sudo_find_delete "/Library/Caches" "*.tmp" "$MOLE_TEMP_FILE_AGE_DAYS" "f" && cache_cleaned=1 || true
|
||||
safe_sudo_find_delete "/Library/Caches" "*.log" "$MOLE_LOG_AGE_DAYS" "f" && cache_cleaned=1 || true
|
||||
[[ $cache_cleaned -eq 1 ]] && log_success "System caches"
|
||||
|
||||
# Clean temporary files (macOS /tmp is a symlink to /private/tmp)
|
||||
local tmp_cleaned=0
|
||||
safe_sudo_find_delete "/private/tmp" "*" "${MOLE_TEMP_FILE_AGE_DAYS}" "f" && tmp_cleaned=1 || true
|
||||
safe_sudo_find_delete "/private/var/tmp" "*" "${MOLE_TEMP_FILE_AGE_DAYS}" "f" && tmp_cleaned=1 || true
|
||||
[[ $tmp_cleaned -eq 1 ]] && log_success "System temp files"
|
||||
|
||||
# Clean crash reports
|
||||
safe_sudo_find_delete "/Library/Logs/DiagnosticReports" "*" "$MOLE_CRASH_REPORT_AGE_DAYS" "f" || true
|
||||
log_success "System crash reports"
|
||||
|
||||
# Clean system logs (macOS /var is a symlink to /private/var)
|
||||
safe_sudo_find_delete "/private/var/log" "*.log" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
safe_sudo_find_delete "/private/var/log" "*.gz" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
log_success "System logs"
|
||||
|
||||
# Clean Library Updates safely (skip if SIP is enabled)
|
||||
if [[ -d "/Library/Updates" && ! -L "/Library/Updates" ]]; then
|
||||
if ! is_sip_enabled; then
|
||||
@@ -41,14 +34,12 @@ clean_deep_system() {
|
||||
debug_log "Skipping malformed path: $item"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip system-protected files (restricted flag)
|
||||
local item_flags
|
||||
item_flags=$($STAT_BSD -f%Sf "$item" 2> /dev/null || echo "")
|
||||
if [[ "$item_flags" == *"restricted"* ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if safe_sudo_remove "$item"; then
|
||||
((updates_cleaned++))
|
||||
fi
|
||||
@@ -56,20 +47,16 @@ clean_deep_system() {
|
||||
[[ $updates_cleaned -gt 0 ]] && log_success "System library updates"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean macOS Install Data (legacy upgrade leftovers)
|
||||
if [[ -d "/macOS Install Data" ]]; then
|
||||
local mtime=$(get_file_mtime "/macOS Install Data")
|
||||
local age_days=$((($(date +%s) - mtime) / 86400))
|
||||
|
||||
debug_log "Found macOS Install Data (age: ${age_days} days)"
|
||||
|
||||
if [[ $age_days -ge 30 ]]; then
|
||||
local size_kb=$(get_path_size_kb "/macOS Install Data")
|
||||
if [[ -n "$size_kb" && "$size_kb" -gt 0 ]]; then
|
||||
local size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
debug_log "Cleaning macOS Install Data: $size_human (${age_days} days old)"
|
||||
|
||||
if safe_sudo_remove "/macOS Install Data"; then
|
||||
log_success "macOS Install Data ($size_human)"
|
||||
fi
|
||||
@@ -78,21 +65,18 @@ clean_deep_system() {
|
||||
debug_log "Keeping macOS Install Data (only ${age_days} days old, needs 30+)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean browser code signature caches
|
||||
start_section_spinner "Scanning system caches..."
|
||||
local code_sign_cleaned=0
|
||||
local found_count=0
|
||||
local last_update_time=$(date +%s)
|
||||
local update_interval=2 # Update spinner every 2 seconds instead of every 50 files
|
||||
|
||||
# Efficient stream processing for large directories
|
||||
while IFS= read -r -d '' cache_dir; do
|
||||
if safe_remove "$cache_dir" true; then
|
||||
((code_sign_cleaned++))
|
||||
fi
|
||||
((found_count++))
|
||||
|
||||
# Update progress spinner periodically based on time, not count
|
||||
local current_time=$(date +%s)
|
||||
if [[ $((current_time - last_update_time)) -ge $update_interval ]]; then
|
||||
@@ -100,26 +84,20 @@ clean_deep_system() {
|
||||
last_update_time=$current_time
|
||||
fi
|
||||
done < <(run_with_timeout 5 command find /private/var/folders -type d -name "*.code_sign_clone" -path "*/X/*" -print0 2> /dev/null || true)
|
||||
|
||||
stop_section_spinner
|
||||
|
||||
[[ $code_sign_cleaned -gt 0 ]] && log_success "Browser code signature caches ($code_sign_cleaned items)"
|
||||
|
||||
# Clean system diagnostics logs
|
||||
safe_sudo_find_delete "/private/var/db/diagnostics/Special" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
safe_sudo_find_delete "/private/var/db/diagnostics/Persist" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
safe_sudo_find_delete "/private/var/db/DiagnosticPipeline" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
log_success "System diagnostic logs"
|
||||
|
||||
# Clean power logs
|
||||
safe_sudo_find_delete "/private/var/db/powerlog" "*" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
log_success "Power logs"
|
||||
|
||||
# Clean memory exception reports (can accumulate to 1-2GB, thousands of files)
|
||||
# These track app memory limit violations, safe to clean old ones
|
||||
safe_sudo_find_delete "/private/var/db/reportmemoryexception/MemoryLimitViolations" "*" "30" "f" || true
|
||||
log_success "Memory exception reports"
|
||||
|
||||
# Clean system diagnostic tracev3 logs (can be 1-2GB)
|
||||
# System generates these continuously, safe to clean old ones
|
||||
start_section_spinner "Cleaning diagnostic trace logs..."
|
||||
@@ -128,7 +106,6 @@ clean_deep_system() {
|
||||
safe_sudo_find_delete "/private/var/db/diagnostics/Special" "*.tracev3" "30" "f" && diag_logs_cleaned=1 || true
|
||||
stop_section_spinner
|
||||
[[ $diag_logs_cleaned -eq 1 ]] && log_success "System diagnostic trace logs"
|
||||
|
||||
# Clean core symbolication cache (can be 3-5GB, mostly for crash report debugging)
|
||||
# Will regenerate when needed for crash analysis
|
||||
# Use faster du with timeout instead of get_path_size_kb to avoid hanging
|
||||
@@ -138,11 +115,9 @@ clean_deep_system() {
|
||||
# Quick size check with timeout (max 5 seconds)
|
||||
local symbolication_size_mb=""
|
||||
symbolication_size_mb=$(run_with_timeout 5 du -sm "/System/Library/Caches/com.apple.coresymbolicationd/data" 2> /dev/null | awk '{print $1}')
|
||||
|
||||
# Validate that we got a valid size (non-empty and numeric)
|
||||
if [[ -n "$symbolication_size_mb" && "$symbolication_size_mb" =~ ^[0-9]+$ ]]; then
|
||||
debug_log "Symbolication cache size: ${symbolication_size_mb}MB"
|
||||
|
||||
# Only clean if larger than 1GB (1024MB)
|
||||
if [[ $symbolication_size_mb -gt 1024 ]]; then
|
||||
debug_log "Cleaning symbolication cache (size > 1GB)..."
|
||||
@@ -156,21 +131,17 @@ clean_deep_system() {
|
||||
fi
|
||||
debug_log "Core symbolication cache section completed"
|
||||
}
|
||||
|
||||
# Clean incomplete Time Machine backups
|
||||
clean_time_machine_failed_backups() {
|
||||
local tm_cleaned=0
|
||||
|
||||
# Check if tmutil is available
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Start spinner early (before potentially slow tmutil command)
|
||||
start_section_spinner "Checking Time Machine configuration..."
|
||||
local spinner_active=true
|
||||
|
||||
# Check if Time Machine is configured (with short timeout for faster response)
|
||||
local tm_info
|
||||
tm_info=$(run_with_timeout 2 tmutil destinationinfo 2>&1 || echo "failed")
|
||||
@@ -181,7 +152,6 @@ clean_time_machine_failed_backups() {
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -d "/Volumes" ]]; then
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
@@ -189,7 +159,6 @@ clean_time_machine_failed_backups() {
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Skip if backup is running (check actual Running status, not just daemon existence)
|
||||
if tmutil status 2> /dev/null | grep -q "Running = 1"; then
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
@@ -198,25 +167,21 @@ clean_time_machine_failed_backups() {
|
||||
echo -e " ${YELLOW}!${NC} Time Machine backup in progress, skipping cleanup"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Update spinner message for volume scanning
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
start_section_spinner "Checking backup volumes..."
|
||||
fi
|
||||
|
||||
# Fast pre-scan: check which volumes have Backups.backupdb (avoid expensive tmutil checks)
|
||||
local -a backup_volumes=()
|
||||
for volume in /Volumes/*; do
|
||||
[[ -d "$volume" ]] || continue
|
||||
[[ "$volume" == "/Volumes/MacintoshHD" || "$volume" == "/" ]] && continue
|
||||
[[ -L "$volume" ]] && continue
|
||||
|
||||
# Quick check: does this volume have backup directories?
|
||||
if [[ -d "$volume/Backups.backupdb" ]] || [[ -d "$volume/.MobileBackups" ]]; then
|
||||
backup_volumes+=("$volume")
|
||||
fi
|
||||
done
|
||||
|
||||
# If no backup volumes found, stop spinner and return
|
||||
if [[ ${#backup_volumes[@]} -eq 0 ]]; then
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
@@ -225,7 +190,6 @@ clean_time_machine_failed_backups() {
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Update spinner message: we have potential backup volumes, now scan them
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
start_section_spinner "Scanning backup volumes..."
|
||||
@@ -237,47 +201,38 @@ clean_time_machine_failed_backups() {
|
||||
case "$fs_type" in
|
||||
nfs | smbfs | afpfs | cifs | webdav | unknown) continue ;;
|
||||
esac
|
||||
|
||||
# HFS+ style backups (Backups.backupdb)
|
||||
local backupdb_dir="$volume/Backups.backupdb"
|
||||
if [[ -d "$backupdb_dir" ]]; then
|
||||
while IFS= read -r inprogress_file; do
|
||||
[[ -d "$inprogress_file" ]] || continue
|
||||
|
||||
# Only delete old incomplete backups (safety window)
|
||||
local file_mtime=$(get_file_mtime "$inprogress_file")
|
||||
local current_time=$(date +%s)
|
||||
local hours_old=$(((current_time - file_mtime) / 3600))
|
||||
|
||||
if [[ $hours_old -lt $MOLE_TM_BACKUP_SAFE_HOURS ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local size_kb=$(get_path_size_kb "$inprogress_file")
|
||||
[[ "$size_kb" -le 0 ]] && continue
|
||||
|
||||
# Stop spinner before first output
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
spinner_active=false
|
||||
fi
|
||||
|
||||
local backup_name=$(basename "$inprogress_file")
|
||||
local size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Incomplete backup: $backup_name ${YELLOW}($size_human dry)${NC}"
|
||||
((tm_cleaned++))
|
||||
note_activity
|
||||
continue
|
||||
fi
|
||||
|
||||
# Real deletion
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
echo -e " ${YELLOW}!${NC} tmutil not available, skipping: $backup_name"
|
||||
continue
|
||||
fi
|
||||
|
||||
if tmutil delete "$inprogress_file" 2> /dev/null; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Incomplete backup: $backup_name ${GREEN}($size_human)${NC}"
|
||||
((tm_cleaned++))
|
||||
@@ -290,53 +245,42 @@ clean_time_machine_failed_backups() {
|
||||
fi
|
||||
done < <(run_with_timeout 15 find "$backupdb_dir" -maxdepth 3 -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2> /dev/null || true)
|
||||
fi
|
||||
|
||||
# APFS style backups (.backupbundle or .sparsebundle)
|
||||
for bundle in "$volume"/*.backupbundle "$volume"/*.sparsebundle; do
|
||||
[[ -e "$bundle" ]] || continue
|
||||
[[ -d "$bundle" ]] || continue
|
||||
|
||||
# Check if bundle is mounted
|
||||
local bundle_name=$(basename "$bundle")
|
||||
local mounted_path=$(hdiutil info 2> /dev/null | grep -A 5 "image-path.*$bundle_name" | grep "/Volumes/" | awk '{print $1}' | head -1 || echo "")
|
||||
|
||||
if [[ -n "$mounted_path" && -d "$mounted_path" ]]; then
|
||||
while IFS= read -r inprogress_file; do
|
||||
[[ -d "$inprogress_file" ]] || continue
|
||||
|
||||
# Only delete old incomplete backups (safety window)
|
||||
local file_mtime=$(get_file_mtime "$inprogress_file")
|
||||
local current_time=$(date +%s)
|
||||
local hours_old=$(((current_time - file_mtime) / 3600))
|
||||
|
||||
if [[ $hours_old -lt $MOLE_TM_BACKUP_SAFE_HOURS ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local size_kb=$(get_path_size_kb "$inprogress_file")
|
||||
[[ "$size_kb" -le 0 ]] && continue
|
||||
|
||||
# Stop spinner before first output
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
spinner_active=false
|
||||
fi
|
||||
|
||||
local backup_name=$(basename "$inprogress_file")
|
||||
local size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Incomplete APFS backup in $bundle_name: $backup_name ${YELLOW}($size_human dry)${NC}"
|
||||
((tm_cleaned++))
|
||||
note_activity
|
||||
continue
|
||||
fi
|
||||
|
||||
# Real deletion
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if tmutil delete "$inprogress_file" 2> /dev/null; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Incomplete APFS backup in $bundle_name: $backup_name ${GREEN}($size_human)${NC}"
|
||||
((tm_cleaned++))
|
||||
@@ -351,55 +295,42 @@ clean_time_machine_failed_backups() {
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Stop spinner if still active (no backups found)
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
fi
|
||||
|
||||
if [[ $tm_cleaned -eq 0 ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
fi
|
||||
}
|
||||
|
||||
# Clean local APFS snapshots (older than 24 hours)
|
||||
clean_local_snapshots() {
|
||||
# Check if tmutil is available
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
start_section_spinner "Checking local snapshots..."
|
||||
|
||||
# Check for local snapshots
|
||||
local snapshot_list
|
||||
snapshot_list=$(tmutil listlocalsnapshots / 2> /dev/null)
|
||||
|
||||
stop_section_spinner
|
||||
|
||||
[[ -z "$snapshot_list" ]] && return 0
|
||||
|
||||
# Parse and clean snapshots
|
||||
local cleaned_count=0
|
||||
local total_cleaned_size=0 # Estimation not possible without thin
|
||||
|
||||
# Get current time
|
||||
local current_ts=$(date +%s)
|
||||
local one_day_ago=$((current_ts - 86400))
|
||||
|
||||
while IFS= read -r line; do
|
||||
# Format: com.apple.TimeMachine.2023-10-25-120000
|
||||
if [[ "$line" =~ com\.apple\.TimeMachine\.([0-9]{4})-([0-9]{2})-([0-9]{2})-([0-9]{6}) ]]; then
|
||||
local date_str="${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]} ${BASH_REMATCH[4]:0:2}:${BASH_REMATCH[4]:2:2}:${BASH_REMATCH[4]:4:2}"
|
||||
local snap_ts=$(date -j -f "%Y-%m-%d %H:%M:%S" "$date_str" "+%s" 2> /dev/null || echo "0")
|
||||
|
||||
# Skip if parsing failed
|
||||
[[ "$snap_ts" == "0" ]] && continue
|
||||
|
||||
# If snapshot is older than 24 hours
|
||||
if [[ $snap_ts -lt $one_day_ago ]]; then
|
||||
local snap_name="${BASH_REMATCH[0]}"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Old local snapshot: $snap_name ${YELLOW}(dry)${NC}"
|
||||
((cleaned_count++))
|
||||
@@ -417,7 +348,6 @@ clean_local_snapshots() {
|
||||
fi
|
||||
fi
|
||||
done <<< "$snapshot_list"
|
||||
|
||||
if [[ $cleaned_count -gt 0 && "$DRY_RUN" != "true" ]]; then
|
||||
log_success "Cleaned $cleaned_count old local snapshots"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user