1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 11:31:46 +00:00

feat: remove SQLite vacuum optimization, enhance CJK/emoji width calculation, and improve system cleanup and UI feedback.

This commit is contained in:
Tw93
2025-12-29 00:29:42 +08:00
parent 63b915b234
commit b67204f959
12 changed files with 57 additions and 407 deletions

View File

@@ -255,7 +255,7 @@ Mole uses **BATS (Bash Automated Testing System)** for automated testing.
|---------------|----------|-----------|
| Core File Operations | 95% | Path validation, symlink detection, permissions |
| Cleaning Logic | 87% | Orphan detection, 60-day rule, vendor whitelist |
| Optimization | 82% | SQLite VACUUM, cache cleanup, timeouts |
| Optimization | 82% | Cache cleanup, timeouts |
| System Maintenance | 90% | Time Machine, network volumes, crash recovery |
| Security Controls | 100% | Path traversal, command injection, symlinks |
@@ -315,7 +315,6 @@ Mole relies on standard macOS system binaries (all SIP-protected):
| `tmutil` | Time Machine interaction | Skip TM cleanup |
| `dscacheutil` | System cache rebuilding | Optional optimization |
| `diskutil` | Volume information | Skip network volumes |
| `sqlite3` | Database optimization | Skip SQLite VACUUM |
### Go Dependencies (Interactive Tools)

View File

@@ -105,6 +105,7 @@ files_cleaned=0
total_size_cleaned=0
whitelist_skipped_count=0
# shellcheck disable=SC2329
note_activity() {
if [[ $TRACK_SECTION -eq 1 ]]; then
SECTION_ACTIVITY=1
@@ -113,6 +114,7 @@ note_activity() {
# Cleanup background processes
CLEANUP_DONE=false
# shellcheck disable=SC2329
cleanup() {
local signal="${1:-EXIT}"
local exit_code="${2:-$?}"
@@ -124,7 +126,7 @@ cleanup() {
CLEANUP_DONE=true
# Stop all spinners and clear the line
if [[ -n "$INLINE_SPINNER_PID" ]]; then
if [[ -n "${INLINE_SPINNER_PID:-}" ]] && kill -0 "$INLINE_SPINNER_PID" 2> /dev/null; then
kill "$INLINE_SPINNER_PID" 2> /dev/null || true
wait "$INLINE_SPINNER_PID" 2> /dev/null || true
INLINE_SPINNER_PID=""
@@ -176,6 +178,7 @@ end_section() {
TRACK_SECTION=0
}
# shellcheck disable=SC2329
safe_clean() {
if [[ $# -eq 0 ]]; then
return 0
@@ -767,6 +770,7 @@ main() {
hide_cursor
perform_cleanup
show_cursor
exit 0
}
main "$@"

View File

@@ -142,14 +142,24 @@ func coloredProgressBar(value, max int64, percent float64) string {
return bar + colorReset
}
// Calculate display width considering CJK characters.
// Calculate display width considering CJK characters and Emoji.
func runeWidth(r rune) int {
if r >= 0x4E00 && r <= 0x9FFF ||
r >= 0x3400 && r <= 0x4DBF ||
r >= 0x3040 && r <= 0x30FF ||
r >= 0x31F0 && r <= 0x31FF ||
r >= 0xAC00 && r <= 0xD7AF ||
r >= 0xFF00 && r <= 0xFFEF {
if r >= 0x4E00 && r <= 0x9FFF || // CJK Unified Ideographs
r >= 0x3400 && r <= 0x4DBF || // CJK Extension A
r >= 0x20000 && r <= 0x2A6DF || // CJK Extension B
r >= 0x2A700 && r <= 0x2B73F || // CJK Extension C
r >= 0x2B740 && r <= 0x2B81F || // CJK Extension D
r >= 0x2B820 && r <= 0x2CEAF || // CJK Extension E
r >= 0x3040 && r <= 0x30FF || // Hiragana and Katakana
r >= 0x31F0 && r <= 0x31FF || // Katakana Phonetic Extensions
r >= 0xAC00 && r <= 0xD7AF || // Hangul Syllables
r >= 0xFF00 && r <= 0xFFEF || // Fullwidth Forms
r >= 0x1F300 && r <= 0x1F6FF || // Miscellaneous Symbols and Pictographs (includes Transport)
r >= 0x1F900 && r <= 0x1F9FF || // Supplemental Symbols and Pictographs
r >= 0x2600 && r <= 0x26FF || // Miscellaneous Symbols
r >= 0x2700 && r <= 0x27BF || // Dingbats
r >= 0xFE10 && r <= 0xFE1F || // Vertical Forms
r >= 0x1F000 && r <= 0x1F02F { // Mahjong Tiles
return 2
}
return 1

View File

@@ -128,8 +128,6 @@ EOF
items+=('maintenance_scripts|System Log Rotation|Rotate and compress system logs with newsyslog|true')
items+=('swap_cleanup|Virtual Memory Refresh|Reset swap files and dynamic pager service|true')
items+=('network_optimization|Network Stack Optimization|Refresh DNS, rebuild ARP & restart mDNSResponder|true')
items+=('sqlite_vacuum|SQLite Database Optimization|Optimize user databases with VACUUM to reduce size|true')
# Output items as JSON
local first=true
for item in "${items[@]}"; do

View File

@@ -155,29 +155,6 @@ clean_deep_system() {
fi
fi
debug_log "Core symbolication cache section completed"
# Clean Aliyun/DingTalk security component logs (if exists, can be 2-3GB)
# Only clean if the software is installed
debug_log "Checking Aliyun security components..."
local ali_cleaned=0
# List of Aliyun-related paths to clean
local -a ali_paths=(
"/private/var/root/Library/Application Support/ali_bas/bas_http_info/ali_bas_httpclient"
"/private/var/root/Library/Application Support/Aliedr/logs_dir"
"/private/var/root/Library/Application Support/Aliedr/cache"
)
# Clean each path if exists
for ali_path in "${ali_paths[@]}"; do
if sudo test -e "$ali_path" 2> /dev/null; then
debug_log "Found Aliyun component: $ali_path, removing..."
safe_sudo_remove "$ali_path" && ((ali_cleaned++)) || true
fi
done
debug_log "Aliyun security components check completed, cleaned: $ali_cleaned"
[[ $ali_cleaned -gt 0 ]] && log_success "Aliyun security component data"
}
# Clean incomplete Time Machine backups

View File

@@ -520,6 +520,11 @@ should_protect_path() {
return 0
fi
# Protect Notes cache (search index issues)
if [[ "$path_lower" =~ com\.apple\.notes ]]; then
return 0
fi
# 2. Protect caches critical for system UI rendering
# These caches are essential for modern macOS (Sonoma/Sequoia) system UI rendering
case "$path" in

View File

@@ -429,11 +429,18 @@ bytes_to_human_kb() {
get_brand_name() {
local name="$1"
# Detect if system primary language is Chinese
local is_chinese=false
local sys_lang
sys_lang=$(defaults read -g AppleLanguages 2> /dev/null | grep -o 'zh-Hans\|zh-Hant\|zh' | head -1 || echo "")
[[ -n "$sys_lang" ]] && is_chinese=true
# Detect if system primary language is Chinese (Cached)
if [[ -z "${MOLE_IS_CHINESE_SYSTEM:-}" ]]; then
local sys_lang
sys_lang=$(defaults read -g AppleLanguages 2> /dev/null | grep -o 'zh-Hans\|zh-Hant\|zh' | head -1 || echo "")
if [[ -n "$sys_lang" ]]; then
export MOLE_IS_CHINESE_SYSTEM="true"
else
export MOLE_IS_CHINESE_SYSTEM="false"
fi
fi
local is_chinese="${MOLE_IS_CHINESE_SYSTEM}"
# Return localized names based on system language
if [[ "$is_chinese" == true ]]; then

View File

@@ -284,7 +284,7 @@ stop_inline_spinner() {
# Try graceful TERM first, then force KILL if needed
if kill -0 "$INLINE_SPINNER_PID" 2> /dev/null; then
kill -TERM "$INLINE_SPINNER_PID" 2> /dev/null || true
sleep 0.05 2> /dev/null || true
sleep 0.1 2> /dev/null || true
# Force kill if still running
if kill -0 "$INLINE_SPINNER_PID" 2> /dev/null; then
kill -KILL "$INLINE_SPINNER_PID" 2> /dev/null || true

View File

@@ -69,7 +69,20 @@ opt_cache_refresh() {
# Run periodic maintenance scripts
opt_maintenance_scripts() {
if [[ -t 1 ]]; then
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Rotating logs..."
fi
local success=false
if run_with_timeout 120 sudo newsyslog > /dev/null 2>&1; then
success=true
fi
if [[ -t 1 ]]; then
stop_inline_spinner
fi
if [[ "$success" == "true" ]]; then
echo -e " ${GREEN}${NC} System logs rotated"
else
echo -e " ${YELLOW}!${NC} Failed to rotate logs"
@@ -204,96 +217,6 @@ opt_network_optimization() {
echo -e " ${GREEN}${NC} mDNSResponder optimized"
}
# SQLite database optimization
# Runs VACUUM on safe user-level databases to reduce size and improve performance
# Only operates on databases that are not in use and meet size threshold
opt_sqlite_vacuum() {
local optimized_count=0
local total_saved_kb=0
local min_size_kb=1024 # Only optimize databases larger than 1MB
# List of safe databases to optimize
# Exclude critical system databases like Mail, Messages, Photos
local -a safe_databases=(
"$HOME/Library/Safari/History.db"
"$HOME/Library/Safari/CloudTabs.db"
)
if [[ -t 1 ]]; then
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning databases..."
fi
for db_path in "${safe_databases[@]}"; do
# Check if database exists
[[ ! -f "$db_path" ]] && continue
# Check if it's a SQLite database
if ! file "$db_path" 2> /dev/null | grep -q "SQLite"; then
continue
fi
# Check size threshold
local db_size_kb
db_size_kb=$(get_path_size_kb "$db_path")
[[ $db_size_kb -lt $min_size_kb ]] && continue
# Check if database is in use
if lsof "$db_path" > /dev/null 2>&1; then
debug_log "Skipping $db_path - in use"
continue
fi
# Check disk space (VACUUM needs ~2x database size as temporary space)
local free_space_kb
free_space_kb=$(df -k "$(dirname "$db_path")" | tail -1 | awk '{print $4}')
if [[ $free_space_kb -lt $((db_size_kb * 2)) ]]; then
debug_log "Skipping $db_path - insufficient disk space"
continue
fi
# Verify database integrity before VACUUM
if ! sqlite3 "$db_path" "PRAGMA integrity_check;" 2> /dev/null | grep -q "ok"; then
debug_log "Skipping $db_path - integrity check failed"
continue
fi
# Get size before optimization
local size_before=$db_size_kb
# Perform VACUUM with error handling
if sqlite3 "$db_path" "VACUUM;" 2> /dev/null; then
# Verify database integrity after VACUUM
if ! sqlite3 "$db_path" "PRAGMA integrity_check;" 2> /dev/null | grep -q "ok"; then
debug_log "Warning: $db_path integrity check failed after VACUUM"
continue
fi
# Get size after optimization
local size_after
size_after=$(get_path_size_kb "$db_path")
local saved_kb=$((size_before - size_after))
if [[ $saved_kb -gt 0 ]]; then
((optimized_count++))
((total_saved_kb += saved_kb))
fi
else
debug_log "Failed to VACUUM $db_path"
fi
done
if [[ -t 1 ]]; then
stop_inline_spinner
fi
if [[ $optimized_count -gt 0 ]]; then
local saved_human=$(bytes_to_human "$((total_saved_kb * 1024))")
echo -e " ${GREEN}${NC} Optimized $optimized_count databases ($saved_human saved)"
else
echo -e " ${GREEN}${NC} Databases already optimized"
fi
}
# Clean Spotlight user caches
# Execute optimization by action name
@@ -312,7 +235,6 @@ execute_optimization() {
local_snapshots) opt_local_snapshots ;;
fix_broken_configs) opt_fix_broken_configs ;;
network_optimization) opt_network_optimization ;;
sqlite_vacuum) opt_sqlite_vacuum ;;
*)
echo -e "${YELLOW}${ICON_ERROR}${NC} Unknown action: $action"
return 1

2
mole
View File

@@ -25,7 +25,7 @@ source "$SCRIPT_DIR/lib/core/common.sh"
trap cleanup_temp_files EXIT INT TERM
# Version info
VERSION="1.15.4"
VERSION="1.15.5"
MOLE_TAGLINE="Deep clean and optimize your Mac."
# Check TouchID configuration

View File

@@ -38,9 +38,9 @@ if command -v shellcheck > /dev/null 2>&1; then
SHELL_FILES=$(find . -type f \( -name "*.sh" -o -name "mole" \) -not -path "./tests/*" -not -path "./.git/*")
FILE_COUNT=$(echo "$SHELL_FILES" | wc -l | tr -d ' ')
if shellcheck mole bin/*.sh lib/*.sh scripts/*.sh 2>&1 | grep -q "SC[0-9]"; then
if shellcheck mole bin/*.sh lib/*/*.sh scripts/*.sh 2>&1 | grep -q "SC[0-9]"; then
echo -e "${YELLOW}⚠ ShellCheck found some issues (non-critical):${NC}"
shellcheck mole bin/*.sh lib/*.sh scripts/*.sh 2>&1 | head -20
shellcheck mole bin/*.sh lib/*/*.sh scripts/*.sh 2>&1 | head -20
echo -e "${GREEN}✓ ShellCheck completed (${FILE_COUNT} files checked)${NC}\n"
else
echo -e "${GREEN}✓ ShellCheck passed (${FILE_COUNT} files checked)${NC}\n"

View File

@@ -469,275 +469,3 @@ EOF
[ "$status" -eq 0 ]
[[ "$output" == *"WOULD_CLEAN=no"* ]]
}
@test "clean_deep_system cleans Aliyun components when present" {
run bash --noprofile --norc <<'EOF'
set -euo pipefail
CLEANED_PATHS=""
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/clean/system.sh"
sudo() {
if [[ "$1" == "test" ]]; then
# Simulate Aliyun paths exist
[[ "$3" == *"ali_bas"* || "$3" == *"Aliedr"* ]] && return 0
return 1
fi
return 0
}
safe_sudo_remove() {
CLEANED_PATHS="$CLEANED_PATHS|$1"
return 0
}
safe_sudo_find_delete() { return 0; }
log_success() { :; }
start_section_spinner() { :; }
stop_section_spinner() { :; }
is_sip_enabled() { return 1; }
find() { return 0; }
run_with_timeout() { shift; "$@"; }
clean_deep_system
echo "$CLEANED_PATHS"
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"ali_bas"* ]]
[[ "$output" == *"Aliedr"* ]]
}
# ============================================================================
# Tests for SQLite VACUUM optimization (v1.15.2)
# ============================================================================
skip_if_no_sqlite3() {
command -v sqlite3 > /dev/null 2>&1 || skip "sqlite3 not available"
}
@test "opt_sqlite_vacuum optimizes Safari databases" {
skip_if_no_sqlite3
# Create test databases
mkdir -p "$HOME/Library/Safari"
# Clean up any existing databases from previous tests
rm -f "$HOME/Library/Safari/History.db" "$HOME/Library/Safari/CloudTabs.db"
# Create a simple SQLite database
run sqlite3 "$HOME/Library/Safari/History.db" "CREATE TABLE test(id INTEGER); INSERT INTO test VALUES(1);"
[ "$status" -eq 0 ]
# Create another test database
run sqlite3 "$HOME/Library/Safari/CloudTabs.db" "CREATE TABLE test(id INTEGER); INSERT INTO test VALUES(1);"
[ "$status" -eq 0 ]
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 functions to avoid actual operations
start_inline_spinner() { :; }
stop_inline_spinner() { :; }
lsof() { return 1; } # Database not in use
opt_sqlite_vacuum
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"Databases already optimized"* || "$output" == *"Optimized"* ]]
}
@test "opt_sqlite_vacuum checks database integrity before VACUUM" {
skip_if_no_sqlite3
mkdir -p "$HOME/Library/Safari"
# Clean up and create database
rm -f "$HOME/Library/Safari/History.db"
sqlite3 "$HOME/Library/Safari/History.db" "CREATE TABLE test(id INTEGER);"
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"
INTEGRITY_CHECKS=0
original_sqlite3=$(command -v sqlite3)
sqlite3() {
if [[ "$2" == *"integrity_check"* ]]; then
((INTEGRITY_CHECKS++))
echo "ok"
return 0
fi
"$original_sqlite3" "$@"
}
export -f sqlite3
start_inline_spinner() { :; }
stop_inline_spinner() { :; }
lsof() { return 1; }
opt_sqlite_vacuum
echo "INTEGRITY_CHECKS=$INTEGRITY_CHECKS"
EOF
[ "$status" -eq 0 ]
# Should check integrity at least once (before VACUUM)
[[ "$output" == *"INTEGRITY_CHECKS="* ]]
}
@test "opt_sqlite_vacuum skips databases in use" {
skip_if_no_sqlite3
mkdir -p "$HOME/Library/Safari"
rm -f "$HOME/Library/Safari/History.db"
sqlite3 "$HOME/Library/Safari/History.db" "CREATE TABLE test(id INTEGER);"
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"
VACUUM_COUNT=0
original_sqlite3=$(command -v sqlite3)
sqlite3() {
if [[ "$2" == "VACUUM;" ]]; then
((VACUUM_COUNT++))
fi
"$original_sqlite3" "$@"
}
export -f sqlite3
start_inline_spinner() { :; }
stop_inline_spinner() { :; }
lsof() { return 0; } # Database is in use
opt_sqlite_vacuum
echo "VACUUM_COUNT=$VACUUM_COUNT"
EOF
[ "$status" -eq 0 ]
# Should not VACUUM when database is in use
[[ "$output" == *"VACUUM_COUNT=0"* ]]
}
@test "opt_sqlite_vacuum checks disk space before VACUUM" {
skip_if_no_sqlite3
mkdir -p "$HOME/Library/Safari"
# Clean up and create a 2MB database
rm -f "$HOME/Library/Safari/History.db"
sqlite3 "$HOME/Library/Safari/History.db" << 'SQL'
CREATE TABLE test(id INTEGER, data TEXT);
INSERT INTO test VALUES(1, randomblob(1024*1024));
INSERT INTO test VALUES(2, randomblob(1024*1024));
SQL
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"
VACUUM_ATTEMPTED=0
original_sqlite3=$(command -v sqlite3)
sqlite3() {
if [[ "$2" == "VACUUM;" ]]; then
VACUUM_ATTEMPTED=1
fi
"$original_sqlite3" "$@"
}
export -f sqlite3
# Mock df to report insufficient space
df() {
if [[ "$1" == "-k" ]]; then
echo "Filesystem 1024-blocks Used Available Capacity"
echo "/dev/disk1 1000000 900000 100 90%" # Only 100KB free
fi
}
export -f df
start_inline_spinner() { :; }
stop_inline_spinner() { :; }
lsof() { return 1; }
opt_sqlite_vacuum
echo "VACUUM_ATTEMPTED=$VACUUM_ATTEMPTED"
EOF
[ "$status" -eq 0 ]
# Should skip VACUUM when insufficient disk space
[[ "$output" == *"VACUUM_ATTEMPTED=0"* ]]
}
@test "opt_sqlite_vacuum skips non-SQLite files" {
skip_if_no_sqlite3
mkdir -p "$HOME/Library/Safari"
# Create a non-SQLite file
echo "not a database" > "$HOME/Library/Safari/History.db"
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"
start_inline_spinner() { :; }
stop_inline_spinner() { :; }
opt_sqlite_vacuum
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"Databases already optimized"* ]]
}
@test "opt_sqlite_vacuum verifies integrity after VACUUM" {
skip_if_no_sqlite3
mkdir -p "$HOME/Library/Safari"
rm -f "$HOME/Library/Safari/History.db"
sqlite3 "$HOME/Library/Safari/History.db" "CREATE TABLE test(id INTEGER); INSERT INTO test VALUES(1);"
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"
CALL_LOG="$HOME/vacuum_calls.log"
> "$CALL_LOG"
original_sqlite3=$(command -v sqlite3)
sqlite3() {
if [[ "$2" == *"integrity_check"* ]]; then
echo "INTEGRITY_CHECK" >> "$CALL_LOG"
echo "ok"
return 0
elif [[ "$2" == "VACUUM;" ]]; then
echo "VACUUM" >> "$CALL_LOG"
fi
"$original_sqlite3" "$@"
}
export -f sqlite3
start_inline_spinner() { :; }
stop_inline_spinner() { :; }
lsof() { return 1; }
opt_sqlite_vacuum
# Count calls
integrity_count=$(grep -c "INTEGRITY_CHECK" "$CALL_LOG" || echo 0)
vacuum_count=$(grep -c "VACUUM" "$CALL_LOG" || echo 0)
echo "INTEGRITY_CHECKS=$integrity_count"
echo "VACUUM_COUNT=$vacuum_count"
EOF
[ "$status" -eq 0 ]
# Should check integrity at least once and perform VACUUM
[[ "$output" == *"INTEGRITY_CHECKS="* ]]
[[ "$output" == *"VACUUM_COUNT="* ]]
}