mirror of
https://github.com/tw93/Mole.git
synced 2026-03-24 04:20:07 +00:00
fix(clean): avoid container cache scan stalls
Add a lightweight top-level entry cap to Containers and Group Containers scanning. When a cache directory exceeds the threshold, skip the expensive per-item du/find size scan and take a partial-size / "cleaned" output path instead. Replace find-piped-to-read loops with pure-bash glob iteration to cut external process overhead. Closes #586
This commit is contained in:
@@ -484,6 +484,32 @@ clean_support_app_data() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# App caches (merged: macOS system caches + Sandboxed apps).
|
# App caches (merged: macOS system caches + Sandboxed apps).
|
||||||
|
cache_top_level_entry_count_capped() {
|
||||||
|
local dir="$1"
|
||||||
|
local cap="${2:-101}"
|
||||||
|
local count=0
|
||||||
|
local _nullglob_state
|
||||||
|
local _dotglob_state
|
||||||
|
_nullglob_state=$(shopt -p nullglob || true)
|
||||||
|
_dotglob_state=$(shopt -p dotglob || true)
|
||||||
|
shopt -s nullglob dotglob
|
||||||
|
|
||||||
|
local item
|
||||||
|
for item in "$dir"/*; do
|
||||||
|
[[ -e "$item" ]] || continue
|
||||||
|
count=$((count + 1))
|
||||||
|
if ((count >= cap)); then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
eval "$_nullglob_state"
|
||||||
|
eval "$_dotglob_state"
|
||||||
|
|
||||||
|
[[ "$count" =~ ^[0-9]+$ ]] || count=0
|
||||||
|
printf '%s\n' "$count"
|
||||||
|
}
|
||||||
|
|
||||||
clean_app_caches() {
|
clean_app_caches() {
|
||||||
start_section_spinner "Scanning app caches..."
|
start_section_spinner "Scanning app caches..."
|
||||||
|
|
||||||
@@ -516,6 +542,7 @@ clean_app_caches() {
|
|||||||
[[ ! -d "$containers_dir" ]] && return 0
|
[[ ! -d "$containers_dir" ]] && return 0
|
||||||
start_section_spinner "Scanning sandboxed apps..."
|
start_section_spinner "Scanning sandboxed apps..."
|
||||||
local total_size=0
|
local total_size=0
|
||||||
|
local total_size_partial=false
|
||||||
local cleaned_count=0
|
local cleaned_count=0
|
||||||
local found_any=false
|
local found_any=false
|
||||||
|
|
||||||
@@ -529,12 +556,22 @@ clean_app_caches() {
|
|||||||
stop_section_spinner
|
stop_section_spinner
|
||||||
|
|
||||||
if [[ "$found_any" == "true" ]]; then
|
if [[ "$found_any" == "true" ]]; then
|
||||||
local size_human
|
|
||||||
size_human=$(bytes_to_human "$((total_size * 1024))")
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Sandboxed app caches${NC}, ${YELLOW}$size_human dry${NC}"
|
if [[ "$total_size_partial" == "true" ]]; then
|
||||||
|
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Sandboxed app caches${NC}, ${YELLOW}dry${NC}"
|
||||||
|
else
|
||||||
|
local size_human
|
||||||
|
size_human=$(bytes_to_human "$((total_size * 1024))")
|
||||||
|
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Sandboxed app caches${NC}, ${YELLOW}$size_human dry${NC}"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Sandboxed app caches${NC}, ${GREEN}$size_human${NC}"
|
if [[ "$total_size_partial" == "true" ]]; then
|
||||||
|
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Sandboxed app caches${NC}, ${GREEN}cleaned${NC}"
|
||||||
|
else
|
||||||
|
local size_human
|
||||||
|
size_human=$(bytes_to_human "$((total_size * 1024))")
|
||||||
|
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Sandboxed app caches${NC}, ${GREEN}$size_human${NC}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
files_cleaned=$((files_cleaned + cleaned_count))
|
files_cleaned=$((files_cleaned + cleaned_count))
|
||||||
total_size_cleaned=$((total_size_cleaned + total_size))
|
total_size_cleaned=$((total_size_cleaned + total_size))
|
||||||
@@ -561,20 +598,35 @@ process_container_cache() {
|
|||||||
local cache_dir="$container_dir/Data/Library/Caches"
|
local cache_dir="$container_dir/Data/Library/Caches"
|
||||||
[[ -d "$cache_dir" ]] || return 0
|
[[ -d "$cache_dir" ]] || return 0
|
||||||
[[ -L "$cache_dir" ]] && return 0
|
[[ -L "$cache_dir" ]] && return 0
|
||||||
# Fast non-empty check.
|
local item_count
|
||||||
if find "$cache_dir" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
item_count=$(cache_top_level_entry_count_capped "$cache_dir" 101)
|
||||||
|
[[ "$item_count" =~ ^[0-9]+$ ]] || item_count=0
|
||||||
|
[[ "$item_count" -eq 0 ]] && return 0
|
||||||
|
|
||||||
|
if [[ "$item_count" -le 100 ]]; then
|
||||||
local size
|
local size
|
||||||
size=$(get_path_size_kb "$cache_dir")
|
size=$(get_path_size_kb "$cache_dir" 2> /dev/null || echo "0")
|
||||||
|
[[ "$size" =~ ^[0-9]+$ ]] || size=0
|
||||||
total_size=$((total_size + size))
|
total_size=$((total_size + size))
|
||||||
found_any=true
|
else
|
||||||
cleaned_count=$((cleaned_count + 1))
|
total_size_partial=true
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
fi
|
||||||
local item
|
|
||||||
while IFS= read -r -d '' item; do
|
found_any=true
|
||||||
[[ -e "$item" ]] || continue
|
cleaned_count=$((cleaned_count + 1))
|
||||||
safe_remove "$item" true || true
|
if [[ "$DRY_RUN" != "true" ]]; then
|
||||||
done < <(command find "$cache_dir" -mindepth 1 -maxdepth 1 -print0 2> /dev/null || true)
|
local _nullglob_state
|
||||||
fi
|
local _dotglob_state
|
||||||
|
_nullglob_state=$(shopt -p nullglob || true)
|
||||||
|
_dotglob_state=$(shopt -p dotglob || true)
|
||||||
|
shopt -s nullglob dotglob
|
||||||
|
local item
|
||||||
|
for item in "$cache_dir"/*; do
|
||||||
|
[[ -e "$item" ]] || continue
|
||||||
|
safe_remove "$item" true || true
|
||||||
|
done
|
||||||
|
eval "$_nullglob_state"
|
||||||
|
eval "$_dotglob_state"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,6 +640,7 @@ clean_group_container_caches() {
|
|||||||
|
|
||||||
start_section_spinner "Scanning Group Containers..."
|
start_section_spinner "Scanning Group Containers..."
|
||||||
local total_size=0
|
local total_size=0
|
||||||
|
local total_size_partial=false
|
||||||
local cleaned_count=0
|
local cleaned_count=0
|
||||||
local found_any=false
|
local found_any=false
|
||||||
|
|
||||||
@@ -642,42 +695,56 @@ clean_group_container_caches() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build non-protected candidate items for cleanup.
|
|
||||||
local -a items_to_clean=()
|
|
||||||
local item
|
local item
|
||||||
while IFS= read -r -d '' item; do
|
local quick_count
|
||||||
[[ -e "$item" ]] || continue
|
quick_count=$(cache_top_level_entry_count_capped "$candidate" 101)
|
||||||
[[ -L "$item" ]] && continue
|
[[ "$quick_count" =~ ^[0-9]+$ ]] || quick_count=0
|
||||||
if should_protect_path "$item" 2> /dev/null || is_path_whitelisted "$item" 2> /dev/null; then
|
[[ "$quick_count" -eq 0 ]] && continue
|
||||||
continue
|
|
||||||
else
|
|
||||||
items_to_clean+=("$item")
|
|
||||||
fi
|
|
||||||
done < <(command find "$candidate" -mindepth 1 -maxdepth 1 -print0 2> /dev/null || true)
|
|
||||||
|
|
||||||
[[ ${#items_to_clean[@]} -gt 0 ]] || continue
|
|
||||||
|
|
||||||
local candidate_size_kb=0
|
local candidate_size_kb=0
|
||||||
local candidate_changed=false
|
local candidate_changed=false
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
local _nullglob_state
|
||||||
for item in "${items_to_clean[@]}"; do
|
local _dotglob_state
|
||||||
local item_size
|
_nullglob_state=$(shopt -p nullglob || true)
|
||||||
item_size=$(get_path_size_kb "$item" 2> /dev/null) || item_size=0
|
_dotglob_state=$(shopt -p dotglob || true)
|
||||||
[[ "$item_size" =~ ^[0-9]+$ ]] || item_size=0
|
shopt -s nullglob dotglob
|
||||||
|
|
||||||
|
if [[ "$quick_count" -gt 100 ]]; then
|
||||||
|
total_size_partial=true
|
||||||
|
for item in "$candidate"/*; do
|
||||||
|
[[ -e "$item" ]] || continue
|
||||||
|
[[ -L "$item" ]] && continue
|
||||||
|
if should_protect_path "$item" 2> /dev/null || is_path_whitelisted "$item" 2> /dev/null; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
candidate_changed=true
|
candidate_changed=true
|
||||||
candidate_size_kb=$((candidate_size_kb + item_size))
|
if [[ "$DRY_RUN" != "true" ]]; then
|
||||||
|
safe_remove "$item" true 2> /dev/null || true
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
for item in "${items_to_clean[@]}"; do
|
for item in "$candidate"/*; do
|
||||||
|
[[ -e "$item" ]] || continue
|
||||||
|
[[ -L "$item" ]] && continue
|
||||||
|
if should_protect_path "$item" 2> /dev/null || is_path_whitelisted "$item" 2> /dev/null; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
local item_size
|
local item_size
|
||||||
item_size=$(get_path_size_kb "$item" 2> /dev/null) || item_size=0
|
item_size=$(get_path_size_kb "$item" 2> /dev/null) || item_size=0
|
||||||
[[ "$item_size" =~ ^[0-9]+$ ]] || item_size=0
|
[[ "$item_size" =~ ^[0-9]+$ ]] || item_size=0
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
candidate_changed=true
|
||||||
|
candidate_size_kb=$((candidate_size_kb + item_size))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
if safe_remove "$item" true 2> /dev/null; then
|
if safe_remove "$item" true 2> /dev/null; then
|
||||||
candidate_changed=true
|
candidate_changed=true
|
||||||
candidate_size_kb=$((candidate_size_kb + item_size))
|
candidate_size_kb=$((candidate_size_kb + item_size))
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
eval "$_nullglob_state"
|
||||||
|
eval "$_dotglob_state"
|
||||||
|
|
||||||
if [[ "$candidate_changed" == "true" ]]; then
|
if [[ "$candidate_changed" == "true" ]]; then
|
||||||
total_size=$((total_size + candidate_size_kb))
|
total_size=$((total_size + candidate_size_kb))
|
||||||
@@ -690,12 +757,22 @@ clean_group_container_caches() {
|
|||||||
stop_section_spinner
|
stop_section_spinner
|
||||||
|
|
||||||
if [[ "$found_any" == "true" ]]; then
|
if [[ "$found_any" == "true" ]]; then
|
||||||
local size_human
|
|
||||||
size_human=$(bytes_to_human "$((total_size * 1024))")
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Group Containers logs/caches${NC}, ${YELLOW}$size_human dry${NC}"
|
if [[ "$total_size_partial" == "true" ]]; then
|
||||||
|
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Group Containers logs/caches${NC}, ${YELLOW}dry${NC}"
|
||||||
|
else
|
||||||
|
local size_human
|
||||||
|
size_human=$(bytes_to_human "$((total_size * 1024))")
|
||||||
|
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Group Containers logs/caches${NC}, ${YELLOW}$size_human dry${NC}"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Group Containers logs/caches${NC}, ${GREEN}$size_human${NC}"
|
if [[ "$total_size_partial" == "true" ]]; then
|
||||||
|
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Group Containers logs/caches${NC}, ${GREEN}cleaned${NC}"
|
||||||
|
else
|
||||||
|
local size_human
|
||||||
|
size_human=$(bytes_to_human "$((total_size * 1024))")
|
||||||
|
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Group Containers logs/caches${NC}, ${GREEN}$size_human${NC}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
files_cleaned=$((files_cleaned + cleaned_count))
|
files_cleaned=$((files_cleaned + cleaned_count))
|
||||||
total_size_cleaned=$((total_size_cleaned + total_size))
|
total_size_cleaned=$((total_size_cleaned + total_size))
|
||||||
|
|||||||
@@ -193,6 +193,39 @@ EOF
|
|||||||
[[ "$output" != *"App caches"* ]] || [[ "$output" == *"already clean"* ]]
|
[[ "$output" != *"App caches"* ]] || [[ "$output" == *"already clean"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "clean_app_caches skips expensive size scans for large sandboxed caches" {
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=true /bin/bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
|
source "$PROJECT_ROOT/lib/clean/user.sh"
|
||||||
|
start_section_spinner() { :; }
|
||||||
|
stop_section_spinner() { :; }
|
||||||
|
bytes_to_human() { echo "0B"; }
|
||||||
|
note_activity() { :; }
|
||||||
|
safe_clean() { :; }
|
||||||
|
should_protect_data() { return 1; }
|
||||||
|
is_critical_system_component() { return 1; }
|
||||||
|
get_path_size_kb() {
|
||||||
|
echo "SHOULD_NOT_SIZE_SCAN"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
files_cleaned=0
|
||||||
|
total_size_cleaned=0
|
||||||
|
total_items=0
|
||||||
|
|
||||||
|
mkdir -p "$HOME/Library/Containers/com.example.large/Data/Library/Caches"
|
||||||
|
for i in $(seq 1 101); do
|
||||||
|
touch "$HOME/Library/Containers/com.example.large/Data/Library/Caches/file-$i.tmp"
|
||||||
|
done
|
||||||
|
|
||||||
|
clean_app_caches
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Sandboxed app caches"* ]]
|
||||||
|
[[ "$output" != *"SHOULD_NOT_SIZE_SCAN"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
@test "clean_application_support_logs counts nested directory contents in dry-run size summary" {
|
@test "clean_application_support_logs counts nested directory contents in dry-run size summary" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=true bash --noprofile --norc <<'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=true bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -451,6 +484,36 @@ EOF
|
|||||||
[[ "$output" != *"Group Containers logs/caches"* ]]
|
[[ "$output" != *"Group Containers logs/caches"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "clean_group_container_caches skips per-item size scans for large candidates" {
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=true /bin/bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
|
source "$PROJECT_ROOT/lib/clean/user.sh"
|
||||||
|
start_section_spinner() { :; }
|
||||||
|
stop_section_spinner() { :; }
|
||||||
|
bytes_to_human() { echo "0B"; }
|
||||||
|
note_activity() { :; }
|
||||||
|
get_path_size_kb() {
|
||||||
|
echo "SHOULD_NOT_SIZE_SCAN"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
files_cleaned=0
|
||||||
|
total_size_cleaned=0
|
||||||
|
total_items=0
|
||||||
|
|
||||||
|
mkdir -p "$HOME/Library/Group Containers/group.com.example.large/Library/Caches"
|
||||||
|
for i in $(seq 1 101); do
|
||||||
|
touch "$HOME/Library/Group Containers/group.com.example.large/Library/Caches/file-$i.tmp"
|
||||||
|
done
|
||||||
|
|
||||||
|
clean_group_container_caches
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Group Containers logs/caches"* ]]
|
||||||
|
[[ "$output" != *"SHOULD_NOT_SIZE_SCAN"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
@test "clean_finder_metadata respects protection flag" {
|
@test "clean_finder_metadata respects protection flag" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PROTECT_FINDER_METADATA=true /bin/bash --noprofile --norc <<'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PROTECT_FINDER_METADATA=true /bin/bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|||||||
Reference in New Issue
Block a user