From 300aded07b16068419cdab9eed14320f44ed7d01 Mon Sep 17 00:00:00 2001 From: tw93 Date: Sat, 7 Mar 2026 18:35:19 +0800 Subject: [PATCH] fix(clean): avoid stalls in app support scan --- lib/clean/user.sh | 23 +++++++++++++-- tests/clean_user_core.bats | 57 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/lib/clean/user.sh b/lib/clean/user.sh index a5a1ecb..60e2af9 100644 --- a/lib/clean/user.sh +++ b/lib/clean/user.sh @@ -752,6 +752,23 @@ clean_virtualization_tools() { # Estimate item size for Application Support cleanup. # Files use stat; directories use du with timeout to avoid long blocking scans. +app_support_entry_count_capped() { + local dir="$1" + local maxdepth="${2:-1}" + local cap="${3:-101}" + local count=0 + + while IFS= read -r -d '' _entry; do + count=$((count + 1)) + if ((count >= cap)); then + break + fi + done < <(command find "$dir" -mindepth 1 -maxdepth "$maxdepth" -print0 2> /dev/null) + + [[ "$count" =~ ^[0-9]+$ ]] || count=0 + printf '%s\n' "$count" +} + app_support_item_size_bytes() { local item="$1" local timeout_seconds="${2:-0.4}" @@ -768,7 +785,7 @@ app_support_item_size_bytes() { # Fast path: if directory has too many items, skip detailed size calculation # to avoid hanging on deep directories (e.g., node_modules, .git) local item_count - item_count=$(command find "$item" -maxdepth 2 -print0 2> /dev/null | tr -d '\0' | wc -c) + item_count=$(app_support_entry_count_capped "$item" 2 10001) if [[ "$item_count" -gt 10000 ]]; then # Return 1 to signal "too many items, size unknown" return 1 @@ -859,7 +876,7 @@ clean_application_support_logs() { if [[ -d "$candidate" ]]; then # Quick count check - skip if too many items to avoid hanging local quick_count - quick_count=$(command find "$candidate" -mindepth 1 -maxdepth 1 -printf '1\n' 2> /dev/null | wc -l | tr -d ' ') + quick_count=$(app_support_entry_count_capped "$candidate" 1 101) if [[ "$quick_count" -gt 100 ]]; then # Too many items - use bulk removal instead of item-by-item local app_label="$app_name" @@ -935,7 +952,7 @@ clean_application_support_logs() { if [[ -d "$candidate" ]]; then # Quick count check - skip if too many items local quick_count - quick_count=$(command find "$candidate" -mindepth 1 -maxdepth 1 -printf '1\n' 2> /dev/null | wc -l | tr -d ' ') + quick_count=$(app_support_entry_count_capped "$candidate" 1 101) if [[ "$quick_count" -gt 100 ]]; then local container_label="$container" if [[ ${#container_label} -gt 24 ]]; then diff --git a/tests/clean_user_core.bats b/tests/clean_user_core.bats index 29b5f45..acf3b54 100644 --- a/tests/clean_user_core.bats +++ b/tests/clean_user_core.bats @@ -220,6 +220,63 @@ EOF [[ "$total_kb" -ge 2 ]] } +@test "clean_application_support_logs uses bulk clean for large Application Support directories" { + local support_home="$HOME/support-appsupport-bulk" + run env HOME="$support_home" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=true bash --noprofile --norc <<'EOF' +set -euo pipefail +mkdir -p "$HOME" +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/clean/user.sh" +start_section_spinner() { echo "SPIN:$1"; } +stop_section_spinner() { :; } +note_activity() { :; } +safe_remove() { echo "REMOVE:$1"; } +update_progress_if_needed() { return 1; } +should_protect_data() { return 1; } +is_critical_system_component() { return 1; } +bytes_to_human() { echo "0B"; } +files_cleaned=0 +total_size_cleaned=0 +total_items=0 + +mkdir -p "$HOME/Library/Application Support/adspower_global/logs" +for i in $(seq 1 101); do + touch "$HOME/Library/Application Support/adspower_global/logs/file-$i.log" +done + +clean_application_support_logs +rm -rf "$HOME/Library/Application Support" +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"SPIN:Scanning Application Support... 1/1 [adspower_global, bulk clean]"* ]] + [[ "$output" == *"Application Support logs/caches"* ]] + [[ "$output" != *"151250 items"* ]] + [[ "$output" != *"REMOVE:"* ]] +} + +@test "app_support_entry_count_capped stops at cap without failing under pipefail" { + local support_home="$HOME/support-appsupport-cap" + run env HOME="$support_home" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' +set -euo pipefail +mkdir -p "$HOME" +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/clean/user.sh" + +mkdir -p "$HOME/Library/Application Support/adspower_global/logs" +for i in $(seq 1 150); do + touch "$HOME/Library/Application Support/adspower_global/logs/file-$i.log" +done + +count=$(app_support_entry_count_capped "$HOME/Library/Application Support/adspower_global/logs" 1 101) +echo "COUNT=$count" +rm -rf "$HOME/Library/Application Support" +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"COUNT=101"* ]] +} + @test "clean_group_container_caches keeps protected caches and cleans non-protected caches" { run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=false /bin/bash --noprofile --norc <<'EOF' set -euo pipefail