1
0
mirror of https://github.com/tw93/Mole.git synced 2026-03-22 17:55:08 +00:00

fix(clean): harden project cache scans

Refs #541
This commit is contained in:
Tw93
2026-03-06 07:49:44 +08:00
parent 7e69a4eb71
commit 92ad46a396
4 changed files with 361 additions and 166 deletions

View File

@@ -1,6 +1,9 @@
#!/bin/bash
# Cache Cleanup Module
set -euo pipefail
# shellcheck disable=SC1091
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/purge_shared.sh"
# Preflight TCC prompts once to avoid mid-run interruptions.
check_tcc_permissions() {
[[ -t 1 ]] || return 0
@@ -88,154 +91,138 @@ clean_service_worker_cache() {
fi
fi
}
# Next.js/Python project caches with tight scan bounds and timeouts.
clean_project_caches() {
stop_inline_spinner 2> /dev/null || true
# Fast pre-check before scanning the whole home dir.
local has_dev_projects=false
local -a common_dev_dirs=(
"$HOME/Code"
"$HOME/Projects"
"$HOME/workspace"
"$HOME/github"
"$HOME/dev"
"$HOME/work"
"$HOME/src"
"$HOME/repos"
"$HOME/Developer"
"$HOME/Development"
"$HOME/www"
"$HOME/golang"
"$HOME/go"
"$HOME/rust"
"$HOME/python"
"$HOME/ruby"
"$HOME/java"
"$HOME/dotnet"
"$HOME/node"
)
for dir in "${common_dev_dirs[@]}"; do
if [[ -d "$dir" ]]; then
has_dev_projects=true
break
# Check whether a directory looks like a project container.
project_cache_has_indicators() {
local dir="$1"
local max_depth="${2:-5}"
local indicator_timeout="${MOLE_PROJECT_CACHE_DISCOVERY_TIMEOUT:-2}"
[[ -d "$dir" ]] || return 1
local -a find_args=("$dir" "-maxdepth" "$max_depth" "(")
local first=true
local indicator
for indicator in "${MOLE_PURGE_PROJECT_INDICATORS[@]}"; do
if [[ "$first" == "true" ]]; then
first=false
else
find_args+=("-o")
fi
find_args+=("-name" "$indicator")
done
find_args+=(")" "-print" "-quit")
run_with_timeout "$indicator_timeout" find "${find_args[@]}" 2> /dev/null | grep -q .
}
# Discover candidate project roots without scanning the whole home directory.
discover_project_cache_roots() {
local -a roots=()
local root
for root in "${MOLE_PURGE_DEFAULT_SEARCH_PATHS[@]}"; do
[[ -d "$root" ]] && roots+=("$root")
done
while IFS= read -r root; do
[[ -d "$root" ]] && roots+=("$root")
done < <(mole_purge_read_paths_config "$HOME/.config/mole/purge_paths")
local dir
local base
for dir in "$HOME"/*/; do
[[ -d "$dir" ]] || continue
dir="${dir%/}"
base=$(basename "$dir")
case "$base" in
.* | Library | Applications | Movies | Music | Pictures | Public)
continue
;;
esac
if project_cache_has_indicators "$dir" 5; then
roots+=("$dir")
fi
done
# Fallback: look for project markers near $HOME.
if [[ "$has_dev_projects" == "false" ]]; then
local -a project_markers=(
"node_modules"
".git"
"target"
"go.mod"
"Cargo.toml"
"package.json"
"pom.xml"
"build.gradle"
)
local spinner_active=false
if [[ -t 1 ]]; then
MOLE_SPINNER_PREFIX=" "
start_inline_spinner "Detecting dev projects..."
spinner_active=true
fi
for marker in "${project_markers[@]}"; do
if run_with_timeout 3 sh -c "find '$HOME' -maxdepth 2 -name '$marker' -not -path '*/Library/*' -not -path '*/.Trash/*' 2>/dev/null | head -1" | grep -q .; then
has_dev_projects=true
break
fi
done
if [[ "$spinner_active" == "true" ]]; then
stop_inline_spinner 2> /dev/null || true
# Extra clear to prevent spinner character remnants in terminal
[[ -t 1 ]] && printf "\r\033[2K" >&2 || true
fi
[[ "$has_dev_projects" == "false" ]] && return 0
[[ ${#roots[@]} -eq 0 ]] && return 0
printf '%s\n' "${roots[@]}" | LC_ALL=C sort -u
}
# Scan a project root for supported build caches while pruning heavy subtrees.
scan_project_cache_root() {
local root="$1"
local output_file="$2"
local scan_timeout="${MOLE_PROJECT_CACHE_SCAN_TIMEOUT:-6}"
[[ -d "$root" ]] || return 0
local -a find_args=(
find -P "$root" -maxdepth 9 -mount
"(" -name "Library" -o -name ".Trash" -o -name "node_modules" -o -name ".git" -o -name ".svn" -o -name ".hg" -o -name ".venv" -o -name "venv" -o -name ".pnpm-store" -o -name ".fvm" -o -name "DerivedData" -o -name "Pods" ")"
-prune -o
-type d
"(" -name ".next" -o -name "__pycache__" -o -name ".dart_tool" ")"
-print
)
local status=0
run_with_timeout "$scan_timeout" "${find_args[@]}" >> "$output_file" 2> /dev/null || status=$?
if [[ $status -eq 124 ]]; then
debug_log "Project cache scan timed out: $root"
elif [[ $status -ne 0 ]]; then
debug_log "Project cache scan failed (${status}): $root"
fi
return 0
}
# Next.js/Python/Flutter project caches scoped to discovered project roots.
clean_project_caches() {
stop_inline_spinner 2> /dev/null || true
local matches_tmp_file
matches_tmp_file=$(create_temp_file)
local -a scan_roots=()
local root
while IFS= read -r root; do
[[ -n "$root" ]] && scan_roots+=("$root")
done < <(discover_project_cache_roots)
[[ ${#scan_roots[@]} -eq 0 ]] && return 0
if [[ -t 1 ]]; then
MOLE_SPINNER_PREFIX=" "
start_inline_spinner "Searching project caches..."
fi
local nextjs_tmp_file
nextjs_tmp_file=$(create_temp_file)
local pycache_tmp_file
pycache_tmp_file=$(create_temp_file)
local flutter_tmp_file
flutter_tmp_file=$(create_temp_file)
local find_timeout=30
# Parallel scans (Next.js and __pycache__).
# Note: -maxdepth must come before -name for BSD find compatibility
(
command find -P "$HOME" -maxdepth 3 -mount -type d -name ".next" \
-not -path "*/Library/*" \
-not -path "*/.Trash/*" \
-not -path "*/node_modules/*" \
-not -path "*/.*" \
2> /dev/null || true
) > "$nextjs_tmp_file" 2>&1 &
local next_pid=$!
(
command find -P "$HOME" -maxdepth 3 -mount -type d -name "__pycache__" \
-not -path "*/Library/*" \
-not -path "*/.Trash/*" \
-not -path "*/node_modules/*" \
-not -path "*/.*" \
2> /dev/null || true
) > "$pycache_tmp_file" 2>&1 &
local py_pid=$!
(
command find -P "$HOME" -maxdepth 5 -mount -type d -name ".dart_tool" \
-not -path "*/Library/*" \
-not -path "*/.Trash/*" \
-not -path "*/node_modules/*" \
-not -path "*/.fvm/*" \
2> /dev/null || true
) > "$flutter_tmp_file" 2>&1 &
local flutter_pid=$!
local elapsed=0
local check_interval=0.2 # Check every 200ms instead of 1s for smoother experience
while [[ $(echo "$elapsed < $find_timeout" | awk '{print ($1 < $2)}') -eq 1 ]]; do
if ! kill -0 $next_pid 2> /dev/null && ! kill -0 $py_pid 2> /dev/null && ! kill -0 $flutter_pid 2> /dev/null; then
break
fi
sleep $check_interval
elapsed=$(echo "$elapsed + $check_interval" | awk '{print $1 + $2}')
done
# Kill stuck scans after timeout.
for pid in $next_pid $py_pid $flutter_pid; do
if kill -0 "$pid" 2> /dev/null; then
kill -TERM "$pid" 2> /dev/null || true
local grace_period=0
while [[ $grace_period -lt 20 ]]; do
if ! kill -0 "$pid" 2> /dev/null; then
break
fi
sleep 0.1
grace_period=$((grace_period + 1))
done
if kill -0 "$pid" 2> /dev/null; then
kill -KILL "$pid" 2> /dev/null || true
fi
wait "$pid" 2> /dev/null || true
else
wait "$pid" 2> /dev/null || true
fi
for root in "${scan_roots[@]}"; do
scan_project_cache_root "$root" "$matches_tmp_file"
done
if [[ -t 1 ]]; then
stop_inline_spinner
fi
while IFS= read -r next_dir; do
[[ -d "$next_dir/cache" ]] && safe_clean "$next_dir/cache"/* "Next.js build cache" || true
done < "$nextjs_tmp_file"
while IFS= read -r pycache; do
[[ -d "$pycache" ]] && safe_clean "$pycache"/* "Python bytecode cache" || true
done < "$pycache_tmp_file"
while IFS= read -r flutter_tool; do
if [[ -d "$flutter_tool" ]]; then
safe_clean "$flutter_tool" "Flutter build cache (.dart_tool)" || true
local build_dir="$(dirname "$flutter_tool")/build"
if [[ -d "$build_dir" ]]; then
safe_clean "$build_dir" "Flutter build cache (build/)" || true
fi
fi
done < "$flutter_tmp_file"
while IFS= read -r cache_dir; do
case "$(basename "$cache_dir")" in
".next")
[[ -d "$cache_dir/cache" ]] && safe_clean "$cache_dir/cache"/* "Next.js build cache" || true
;;
"__pycache__")
[[ -d "$cache_dir" ]] && safe_clean "$cache_dir"/* "Python bytecode cache" || true
;;
".dart_tool")
if [[ -d "$cache_dir" ]]; then
safe_clean "$cache_dir" "Flutter build cache (.dart_tool)" || true
local build_dir="$(dirname "$cache_dir")/build"
if [[ -d "$build_dir" ]]; then
safe_clean "$build_dir" "Flutter build cache (build/)" || true
fi
fi
;;
esac
done < <(LC_ALL=C sort -u "$matches_tmp_file" 2> /dev/null)
}

View File

@@ -20,12 +20,18 @@ readonly MOLE_TIMEOUT_LOADED=1
# Recommendation: Install coreutils for reliable timeout support
# brew install coreutils
#
# Fallback order:
# 1. gtimeout / timeout
# 2. perl helper with dedicated process group cleanup
# 3. shell-based fallback (last resort)
#
# The shell-based fallback has known limitations:
# - May not clean up all child processes
# - Has race conditions in edge cases
# - Less reliable than native timeout command
# - Less reliable than native timeout/perl helper
if [[ -z "${MO_TIMEOUT_INITIALIZED:-}" ]]; then
MO_TIMEOUT_BIN=""
MO_TIMEOUT_PERL_BIN=""
for candidate in gtimeout timeout; do
if command -v "$candidate" > /dev/null 2>&1; then
MO_TIMEOUT_BIN="$candidate"
@@ -36,8 +42,15 @@ if [[ -z "${MO_TIMEOUT_INITIALIZED:-}" ]]; then
fi
done
if [[ -z "$MO_TIMEOUT_BIN" ]] && command -v perl > /dev/null 2>&1; then
MO_TIMEOUT_PERL_BIN="$(command -v perl)"
if [[ "${MO_DEBUG:-0}" == "1" ]]; then
echo "[TIMEOUT] Using perl fallback: $MO_TIMEOUT_PERL_BIN" >&2
fi
fi
# Log warning if no timeout command available
if [[ -z "$MO_TIMEOUT_BIN" ]] && [[ "${MO_DEBUG:-0}" == "1" ]]; then
if [[ -z "$MO_TIMEOUT_BIN" && -z "$MO_TIMEOUT_PERL_BIN" ]] && [[ "${MO_DEBUG:-0}" == "1" ]]; then
echo "[TIMEOUT] No timeout command found, using shell fallback" >&2
echo "[TIMEOUT] Install coreutils for better reliability: brew install coreutils" >&2
fi
@@ -95,6 +108,66 @@ run_with_timeout() {
return $?
fi
# Use perl helper when timeout command is unavailable.
if [[ -n "${MO_TIMEOUT_PERL_BIN:-}" ]]; then
if [[ "${MO_DEBUG:-0}" == "1" ]]; then
echo "[TIMEOUT] Perl fallback, ${duration}s: $*" >&2
fi
"$MO_TIMEOUT_PERL_BIN" -e '
use strict;
use warnings;
use POSIX qw(:sys_wait_h setsid);
use Time::HiRes qw(time sleep);
my $duration = 0 + shift @ARGV;
$duration = 1 if $duration <= 0;
my $pid = fork();
defined $pid or exit 125;
if ($pid == 0) {
setsid() or exit 125;
exec @ARGV;
exit 127;
}
my $deadline = time() + $duration;
while (1) {
my $result = waitpid($pid, WNOHANG);
if ($result == $pid) {
if (WIFEXITED($?)) {
exit WEXITSTATUS($?);
}
if (WIFSIGNALED($?)) {
exit 128 + WTERMSIG($?);
}
exit 1;
}
if (time() >= $deadline) {
kill "TERM", -$pid;
sleep 0.5;
for (1 .. 6) {
$result = waitpid($pid, WNOHANG);
if ($result == $pid) {
exit 124;
}
sleep 0.25;
}
kill "KILL", -$pid;
waitpid($pid, 0);
exit 124;
}
sleep 0.1;
}
' "$duration" "$@"
return $?
fi
# ========================================================================
# Shell-based fallback implementation
# ========================================================================