1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-06 15:27:58 +00:00

feat: log cleanup operations for troubleshooting

This commit is contained in:
tw93
2026-01-26 15:22:07 +08:00
parent e0aba780c8
commit 8c4cd7f82e
7 changed files with 267 additions and 119 deletions

View File

@@ -42,7 +42,7 @@ validate_path_for_deletion() {
# Check symlink target if path is a symbolic link
if [[ -L "$path" ]]; then
local link_target
link_target=$(readlink "$path" 2> /dev/null) || {
link_target=$(readlink "$path" 2>/dev/null) || {
log_error "Cannot read symlink: $path"
return 1
}
@@ -52,16 +52,16 @@ validate_path_for_deletion() {
if [[ "$link_target" != /* ]]; then
local link_dir
link_dir=$(dirname "$path")
resolved_target=$(cd "$link_dir" 2> /dev/null && cd "$(dirname "$link_target")" 2> /dev/null && pwd)/$(basename "$link_target") || resolved_target=""
resolved_target=$(cd "$link_dir" 2>/dev/null && cd "$(dirname "$link_target")" 2>/dev/null && pwd)/$(basename "$link_target") || resolved_target=""
fi
# Validate resolved target against protected paths
if [[ -n "$resolved_target" ]]; then
case "$resolved_target" in
/System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/etc/*)
log_error "Symlink points to protected system path: $path -> $resolved_target"
return 1
;;
/System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/etc/*)
log_error "Symlink points to protected system path: $path -> $resolved_target"
return 1
;;
esac
fi
fi
@@ -88,48 +88,48 @@ validate_path_for_deletion() {
# Allow deletion of coresymbolicationd cache (safe system cache that can be rebuilt)
case "$path" in
/System/Library/Caches/com.apple.coresymbolicationd/data | /System/Library/Caches/com.apple.coresymbolicationd/data/*)
return 0
;;
/System/Library/Caches/com.apple.coresymbolicationd/data | /System/Library/Caches/com.apple.coresymbolicationd/data/*)
return 0
;;
esac
# Allow known safe paths under /private
case "$path" in
/private/tmp | /private/tmp/* | \
/private/var/tmp | /private/var/tmp/* | \
/private/var/log | /private/var/log/* | \
/private/var/folders | /private/var/folders/* | \
/private/var/db/diagnostics | /private/var/db/diagnostics/* | \
/private/var/db/DiagnosticPipeline | /private/var/db/DiagnosticPipeline/* | \
/private/var/db/powerlog | /private/var/db/powerlog/* | \
/private/var/db/reportmemoryexception | /private/var/db/reportmemoryexception/* | \
/private/var/db/receipts/*.bom | /private/var/db/receipts/*.plist)
return 0
;;
/private/tmp | /private/tmp/* | \
/private/var/tmp | /private/var/tmp/* | \
/private/var/log | /private/var/log/* | \
/private/var/folders | /private/var/folders/* | \
/private/var/db/diagnostics | /private/var/db/diagnostics/* | \
/private/var/db/DiagnosticPipeline | /private/var/db/DiagnosticPipeline/* | \
/private/var/db/powerlog | /private/var/db/powerlog/* | \
/private/var/db/reportmemoryexception | /private/var/db/reportmemoryexception/* | \
/private/var/db/receipts/*.bom | /private/var/db/receipts/*.plist)
return 0
;;
esac
# Check path isn't critical system directory
case "$path" in
/ | /bin | /bin/* | /sbin | /sbin/* | /usr | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /usr/lib | /usr/lib/* | /System | /System/* | /Library/Extensions)
log_error "Path validation failed: critical system directory: $path"
return 1
;;
/private)
log_error "Path validation failed: critical system directory: $path"
return 1
;;
/etc | /etc/* | /private/etc | /private/etc/*)
log_error "Path validation failed: /etc contains critical system files: $path"
return 1
;;
/var | /var/db | /var/db/* | /private/var | /private/var/db | /private/var/db/*)
log_error "Path validation failed: /var/db contains system databases: $path"
return 1
;;
/ | /bin | /bin/* | /sbin | /sbin/* | /usr | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /usr/lib | /usr/lib/* | /System | /System/* | /Library/Extensions)
log_error "Path validation failed: critical system directory: $path"
return 1
;;
/private)
log_error "Path validation failed: critical system directory: $path"
return 1
;;
/etc | /etc/* | /private/etc | /private/etc/*)
log_error "Path validation failed: /etc contains critical system files: $path"
return 1
;;
/var | /var/db | /var/db/* | /private/var | /private/var/db | /private/var/db/*)
log_error "Path validation failed: /var/db contains system databases: $path"
return 1
;;
esac
# Check if path is protected (keychains, system settings, etc)
if declare -f should_protect_path > /dev/null 2>&1; then
if declare -f should_protect_path >/dev/null 2>&1; then
if should_protect_path "$path"; then
if [[ "${MO_DEBUG:-0}" == "1" ]]; then
log_warning "Path validation: protected path skipped: $path"
@@ -172,16 +172,16 @@ safe_remove() {
if [[ -e "$path" ]]; then
local size_kb
size_kb=$(get_path_size_kb "$path" 2> /dev/null || echo "0")
size_kb=$(get_path_size_kb "$path" 2>/dev/null || echo "0")
if [[ "$size_kb" -gt 0 ]]; then
file_size=$(bytes_to_human "$((size_kb * 1024))")
fi
if [[ -f "$path" || -d "$path" ]] && ! [[ -L "$path" ]]; then
local mod_time
mod_time=$(stat -f%m "$path" 2> /dev/null || echo "0")
mod_time=$(stat -f%m "$path" 2>/dev/null || echo "0")
local now
now=$(date +%s 2> /dev/null || echo "0")
now=$(date +%s 2>/dev/null || echo "0")
if [[ "$mod_time" -gt 0 && "$now" -gt 0 ]]; then
file_age=$(((now - mod_time) / 86400))
fi
@@ -197,6 +197,18 @@ safe_remove() {
debug_log "Removing: $path"
# Calculate size before deletion for logging
local size_kb=0
local size_human=""
if oplog_enabled; then
if [[ -e "$path" ]]; then
size_kb=$(get_path_size_kb "$path" 2>/dev/null || echo "0")
if [[ "$size_kb" =~ ^[0-9]+$ ]] && [[ "$size_kb" -gt 0 ]]; then
size_human=$(bytes_to_human "$((size_kb * 1024))" 2>/dev/null || echo "${size_kb}KB")
fi
fi
fi
# Perform the deletion
# Use || to capture the exit code so set -e won't abort on rm failures
local error_msg
@@ -204,6 +216,8 @@ safe_remove() {
error_msg=$(rm -rf "$path" 2>&1) || rm_exit=$? # safe_remove
if [[ $rm_exit -eq 0 ]]; then
# Log successful removal
log_operation "${MOLE_CURRENT_COMMAND:-clean}" "REMOVED" "$path" "$size_human"
return 0
else
# Check if it's a permission error
@@ -212,8 +226,10 @@ safe_remove() {
MOLE_PERMISSION_DENIED_COUNT=$((MOLE_PERMISSION_DENIED_COUNT + 1))
export MOLE_PERMISSION_DENIED_COUNT
debug_log "Permission denied: $path, may need Full Disk Access"
log_operation "${MOLE_CURRENT_COMMAND:-clean}" "FAILED" "$path" "permission denied"
else
[[ "$silent" != "true" ]] && log_error "Failed to remove: $path"
log_operation "${MOLE_CURRENT_COMMAND:-clean}" "FAILED" "$path" "error"
fi
return 1
fi
@@ -249,18 +265,18 @@ safe_sudo_remove() {
local file_size=""
local file_age=""
if sudo test -e "$path" 2> /dev/null; then
if sudo test -e "$path" 2>/dev/null; then
local size_kb
size_kb=$(sudo du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0")
size_kb=$(sudo du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
if [[ "$size_kb" -gt 0 ]]; then
file_size=$(bytes_to_human "$((size_kb * 1024))")
fi
if sudo test -f "$path" 2> /dev/null || sudo test -d "$path" 2> /dev/null; then
if sudo test -f "$path" 2>/dev/null || sudo test -d "$path" 2>/dev/null; then
local mod_time
mod_time=$(sudo stat -f%m "$path" 2> /dev/null || echo "0")
mod_time=$(sudo stat -f%m "$path" 2>/dev/null || echo "0")
local now
now=$(date +%s 2> /dev/null || echo "0")
now=$(date +%s 2>/dev/null || echo "0")
if [[ "$mod_time" -gt 0 && "$now" -gt 0 ]]; then
file_age=$(((now - mod_time) / 86400))
fi
@@ -276,11 +292,25 @@ safe_sudo_remove() {
debug_log "Removing, sudo: $path"
# Calculate size before deletion for logging
local size_kb=0
local size_human=""
if oplog_enabled; then
if sudo test -e "$path" 2>/dev/null; then
size_kb=$(sudo du -sk "$path" 2>/dev/null | awk '{print $1}' || echo "0")
if [[ "$size_kb" =~ ^[0-9]+$ ]] && [[ "$size_kb" -gt 0 ]]; then
size_human=$(bytes_to_human "$((size_kb * 1024))" 2>/dev/null || echo "${size_kb}KB")
fi
fi
fi
# Perform the deletion
if sudo rm -rf "$path" 2> /dev/null; then # SAFE: safe_sudo_remove implementation
if sudo rm -rf "$path" 2>/dev/null; then # SAFE: safe_sudo_remove implementation
log_operation "${MOLE_CURRENT_COMMAND:-clean}" "REMOVED" "$path" "$size_human"
return 0
else
log_error "Failed to remove, sudo: $path"
log_operation "${MOLE_CURRENT_COMMAND:-clean}" "FAILED" "$path" "sudo error"
return 1
fi
}
@@ -326,7 +356,7 @@ safe_find_delete() {
continue
fi
safe_remove "$match" true || true
done < <(command find "$base_dir" "${find_args[@]}" -print0 2> /dev/null || true)
done < <(command find "$base_dir" "${find_args[@]}" -print0 2>/dev/null || true)
return 0
}
@@ -339,12 +369,12 @@ safe_sudo_find_delete() {
local type_filter="${4:-f}"
# Validate base directory (use sudo for permission-restricted dirs)
if ! sudo test -d "$base_dir" 2> /dev/null; then
if ! sudo test -d "$base_dir" 2>/dev/null; then
debug_log "Directory does not exist, skipping: $base_dir"
return 0
fi
if sudo test -L "$base_dir" 2> /dev/null; then
if sudo test -L "$base_dir" 2>/dev/null; then
log_error "Refusing to search symlinked directory: $base_dir"
return 1
fi
@@ -368,7 +398,7 @@ safe_sudo_find_delete() {
continue
fi
safe_sudo_remove "$match" || true
done < <(sudo find "$base_dir" "${find_args[@]}" -print0 2> /dev/null || true)
done < <(sudo find "$base_dir" "${find_args[@]}" -print0 2>/dev/null || true)
return 0
}
@@ -388,7 +418,7 @@ get_path_size_kb() {
# Use || echo 0 to ensure failure in du (e.g. permission error) doesn't exit script under set -e
# Pipefail would normally cause the pipeline to fail if du fails, but || handle catches it.
local size
size=$(command du -sk "$path" 2> /dev/null | awk 'NR==1 {print $1; exit}' || true)
size=$(command du -sk "$path" 2>/dev/null | awk 'NR==1 {print $1; exit}' || true)
# Ensure size is a valid number (fix for non-numeric du output)
if [[ "$size" =~ ^[0-9]+$ ]]; then
@@ -409,7 +439,7 @@ calculate_total_size() {
size_kb=$(get_path_size_kb "$file")
((total_kb += size_kb))
fi
done <<< "$files"
done <<<"$files"
echo "$total_kb"
}

View File

@@ -23,10 +23,15 @@ fi
readonly LOG_FILE="${HOME}/.config/mole/mole.log"
readonly DEBUG_LOG_FILE="${HOME}/.config/mole/mole_debug_session.log"
readonly LOG_MAX_SIZE_DEFAULT=1048576 # 1MB
readonly OPERATIONS_LOG_FILE="${HOME}/.config/mole/operations.log"
readonly LOG_MAX_SIZE_DEFAULT=1048576 # 1MB
readonly OPLOG_MAX_SIZE_DEFAULT=5242880 # 5MB
# Ensure log directory and file exist with correct ownership
ensure_user_file "$LOG_FILE"
if [[ "${MO_NO_OPLOG:-}" != "1" ]]; then
ensure_user_file "$OPERATIONS_LOG_FILE"
fi
# ============================================================================
# Log Rotation
@@ -40,9 +45,18 @@ rotate_log_once() {
local max_size="$LOG_MAX_SIZE_DEFAULT"
if [[ -f "$LOG_FILE" ]] && [[ $(get_file_size "$LOG_FILE") -gt "$max_size" ]]; then
mv "$LOG_FILE" "${LOG_FILE}.old" 2> /dev/null || true
mv "$LOG_FILE" "${LOG_FILE}.old" 2>/dev/null || true
ensure_user_file "$LOG_FILE"
fi
# Rotate operations log (5MB limit)
if [[ "${MO_NO_OPLOG:-}" != "1" ]]; then
local oplog_max_size="$OPLOG_MAX_SIZE_DEFAULT"
if [[ -f "$OPERATIONS_LOG_FILE" ]] && [[ $(get_file_size "$OPERATIONS_LOG_FILE") -gt "$oplog_max_size" ]]; then
mv "$OPERATIONS_LOG_FILE" "${OPERATIONS_LOG_FILE}.old" 2>/dev/null || true
ensure_user_file "$OPERATIONS_LOG_FILE"
fi
fi
}
# ============================================================================
@@ -53,9 +67,9 @@ rotate_log_once() {
log_info() {
echo -e "${BLUE}$1${NC}"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] INFO: $1" >> "$LOG_FILE" 2> /dev/null || true
echo "[$timestamp] INFO: $1" >>"$LOG_FILE" 2>/dev/null || true
if [[ "${MO_DEBUG:-}" == "1" ]]; then
echo "[$timestamp] INFO: $1" >> "$DEBUG_LOG_FILE" 2> /dev/null || true
echo "[$timestamp] INFO: $1" >>"$DEBUG_LOG_FILE" 2>/dev/null || true
fi
}
@@ -63,9 +77,9 @@ log_info() {
log_success() {
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] SUCCESS: $1" >> "$LOG_FILE" 2> /dev/null || true
echo "[$timestamp] SUCCESS: $1" >>"$LOG_FILE" 2>/dev/null || true
if [[ "${MO_DEBUG:-}" == "1" ]]; then
echo "[$timestamp] SUCCESS: $1" >> "$DEBUG_LOG_FILE" 2> /dev/null || true
echo "[$timestamp] SUCCESS: $1" >>"$DEBUG_LOG_FILE" 2>/dev/null || true
fi
}
@@ -73,9 +87,9 @@ log_success() {
log_warning() {
echo -e "${YELLOW}$1${NC}"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] WARNING: $1" >> "$LOG_FILE" 2> /dev/null || true
echo "[$timestamp] WARNING: $1" >>"$LOG_FILE" 2>/dev/null || true
if [[ "${MO_DEBUG:-}" == "1" ]]; then
echo "[$timestamp] WARNING: $1" >> "$DEBUG_LOG_FILE" 2> /dev/null || true
echo "[$timestamp] WARNING: $1" >>"$DEBUG_LOG_FILE" 2>/dev/null || true
fi
}
@@ -83,9 +97,9 @@ log_warning() {
log_error() {
echo -e "${YELLOW}${ICON_ERROR}${NC} $1" >&2
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] ERROR: $1" >> "$LOG_FILE" 2> /dev/null || true
echo "[$timestamp] ERROR: $1" >>"$LOG_FILE" 2>/dev/null || true
if [[ "${MO_DEBUG:-}" == "1" ]]; then
echo "[$timestamp] ERROR: $1" >> "$DEBUG_LOG_FILE" 2> /dev/null || true
echo "[$timestamp] ERROR: $1" >>"$DEBUG_LOG_FILE" 2>/dev/null || true
fi
}
@@ -93,10 +107,84 @@ log_error() {
debug_log() {
if [[ "${MO_DEBUG:-}" == "1" ]]; then
echo -e "${GRAY}[DEBUG]${NC} $*" >&2
echo "[$(date '+%Y-%m-%d %H:%M:%S')] DEBUG: $*" >> "$DEBUG_LOG_FILE" 2> /dev/null || true
echo "[$(date '+%Y-%m-%d %H:%M:%S')] DEBUG: $*" >>"$DEBUG_LOG_FILE" 2>/dev/null || true
fi
}
# ============================================================================
# Operation Logging (Enabled by default)
# ============================================================================
# Records all file operations for user troubleshooting
# Disable with MO_NO_OPLOG=1
oplog_enabled() {
[[ "${MO_NO_OPLOG:-}" != "1" ]]
}
# Log an operation to the operations log file
# Usage: log_operation <command> <action> <path> [detail]
# Example: log_operation "clean" "REMOVED" "/path/to/file" "15.2MB"
# Example: log_operation "clean" "SKIPPED" "/path/to/file" "whitelist"
# Example: log_operation "uninstall" "REMOVED" "/Applications/App.app" "150MB"
log_operation() {
# Allow disabling via environment variable
oplog_enabled || return 0
local command="${1:-unknown}" # clean/uninstall/optimize/purge
local action="${2:-UNKNOWN}" # REMOVED/SKIPPED/FAILED/REBUILT
local path="${3:-}"
local detail="${4:-}"
# Skip if no path provided
[[ -z "$path" ]] && return 0
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local log_line="[$timestamp] [$command] $action $path"
[[ -n "$detail" ]] && log_line+=" ($detail)"
echo "$log_line" >>"$OPERATIONS_LOG_FILE" 2>/dev/null || true
}
# Log session start marker
# Usage: log_operation_session_start <command>
log_operation_session_start() {
oplog_enabled || return 0
local command="${1:-mole}"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
{
echo ""
echo "# ========== $command session started at $timestamp =========="
} >>"$OPERATIONS_LOG_FILE" 2>/dev/null || true
}
# Log session end with summary
# Usage: log_operation_session_end <command> <items_count> <total_size>
log_operation_session_end() {
oplog_enabled || return 0
local command="${1:-mole}"
local items="${2:-0}"
local size="${3:-0}"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local size_human=""
if [[ "$size" =~ ^[0-9]+$ ]] && [[ "$size" -gt 0 ]]; then
size_human=$(bytes_to_human "$((size * 1024))" 2>/dev/null || echo "${size}KB")
else
size_human="0B"
fi
{
echo "# ========== $command session ended at $timestamp, $items items, $size_human =========="
} >>"$OPERATIONS_LOG_FILE" 2>/dev/null || true
}
# Enhanced debug logging for operations
debug_operation_start() {
local operation_name="$1"
@@ -112,7 +200,7 @@ debug_operation_start() {
echo ""
echo "=== $operation_name ==="
[[ -n "$operation_desc" ]] && echo "Description: $operation_desc"
} >> "$DEBUG_LOG_FILE" 2> /dev/null || true
} >>"$DEBUG_LOG_FILE" 2>/dev/null || true
fi
}
@@ -126,7 +214,7 @@ debug_operation_detail() {
echo -e "${GRAY}[DEBUG] $detail_type: $detail_value${NC}" >&2
# Also log to file
echo "$detail_type: $detail_value" >> "$DEBUG_LOG_FILE" 2> /dev/null || true
echo "$detail_type: $detail_value" >>"$DEBUG_LOG_FILE" 2>/dev/null || true
fi
}
@@ -146,7 +234,7 @@ debug_file_action() {
echo -e "${GRAY}[DEBUG] $action: $msg${NC}" >&2
# Also log to file
echo "$action: $msg" >> "$DEBUG_LOG_FILE" 2> /dev/null || true
echo "$action: $msg" >>"$DEBUG_LOG_FILE" 2>/dev/null || true
fi
}
@@ -158,16 +246,16 @@ debug_risk_level() {
if [[ "${MO_DEBUG:-}" == "1" ]]; then
local color="$GRAY"
case "$risk_level" in
LOW) color="$GREEN" ;;
MEDIUM) color="$YELLOW" ;;
HIGH) color="$RED" ;;
LOW) color="$GREEN" ;;
MEDIUM) color="$YELLOW" ;;
HIGH) color="$RED" ;;
esac
# Output to stderr with color
echo -e "${GRAY}[DEBUG] Risk Level: ${color}${risk_level}${GRAY}, $reason${NC}" >&2
# Also log to file
echo "Risk Level: $risk_level, $reason" >> "$DEBUG_LOG_FILE" 2> /dev/null || true
echo "Risk Level: $risk_level, $reason" >>"$DEBUG_LOG_FILE" 2>/dev/null || true
fi
}
@@ -179,7 +267,7 @@ log_system_info() {
# Reset debug log file for this new session
ensure_user_file "$DEBUG_LOG_FILE"
if ! : > "$DEBUG_LOG_FILE" 2> /dev/null; then
if ! : >"$DEBUG_LOG_FILE" 2>/dev/null; then
echo -e "${YELLOW}${ICON_WARNING}${NC} Debug log not writable: $DEBUG_LOG_FILE" >&2
fi
@@ -192,19 +280,19 @@ log_system_info() {
echo "Hostname: $(hostname)"
echo "Architecture: $(uname -m)"
echo "Kernel: $(uname -r)"
if command -v sw_vers > /dev/null; then
if command -v sw_vers >/dev/null; then
echo "macOS: $(sw_vers -productVersion), $(sw_vers -buildVersion)"
fi
echo "Shell: ${SHELL:-unknown}, ${TERM:-unknown}"
# Check sudo status non-interactively
if sudo -n true 2> /dev/null; then
if sudo -n true 2>/dev/null; then
echo "Sudo Access: Active"
else
echo "Sudo Access: Required"
fi
echo "----------------------------------------------------------------------"
} >> "$DEBUG_LOG_FILE" 2> /dev/null || true
} >>"$DEBUG_LOG_FILE" 2>/dev/null || true
# Notification to stderr
echo -e "${GRAY}[DEBUG] Debug logging enabled. Session log: $DEBUG_LOG_FILE${NC}" >&2
@@ -216,7 +304,7 @@ log_system_info() {
# Run command silently (ignore errors)
run_silent() {
"$@" > /dev/null 2>&1 || true
"$@" >/dev/null 2>&1 || true
}
# Run command with error logging
@@ -224,12 +312,12 @@ run_logged() {
local cmd="$1"
# Log to main file, and also to debug file if enabled
if [[ "${MO_DEBUG:-}" == "1" ]]; then
if ! "$@" 2>&1 | tee -a "$LOG_FILE" | tee -a "$DEBUG_LOG_FILE" > /dev/null; then
if ! "$@" 2>&1 | tee -a "$LOG_FILE" | tee -a "$DEBUG_LOG_FILE" >/dev/null; then
log_warning "Command failed: $cmd"
return 1
fi
else
if ! "$@" 2>&1 | tee -a "$LOG_FILE" > /dev/null; then
if ! "$@" 2>&1 | tee -a "$LOG_FILE" >/dev/null; then
log_warning "Command failed: $cmd"
return 1
fi