mirror of
https://github.com/tw93/Mole.git
synced 2026-02-11 12:59:16 +00:00
feat(uninstall): remove app diagnostic reports from /Library/Logs/DiagnosticReports (fixes #441) (#443)
* Implement deleting files from DiagnosticReports * fix(uninstall): avoid diagnostic size double-count and set -e exit --------- Co-authored-by: tw93 <tw93@qq.com>
This commit is contained in:
@@ -1156,6 +1156,42 @@ find_app_files() {
|
||||
return 0
|
||||
}
|
||||
|
||||
get_diagnostic_report_paths_for_app() {
|
||||
local app_path="$1"
|
||||
local app_name="$2"
|
||||
local directory="$3"
|
||||
local prefix=""
|
||||
local exec_name=""
|
||||
local nospace_name="${app_name// /}"
|
||||
|
||||
[[ -z "$app_path" || -z "$app_name" || -z "$directory" ]] && return 0
|
||||
[[ ! -d "$directory" ]] && return 0
|
||||
|
||||
if [[ -f "$app_path/Contents/Info.plist" ]]; then
|
||||
exec_name=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null || echo "")
|
||||
if [[ -z "$exec_name" ]]; then
|
||||
exec_name=$(grep -A1 "CFBundleExecutable" "$app_path/Contents/Info.plist" 2> /dev/null | grep "<string>" | sed -n 's/.*<string>\([^<]*\)<\/string>.*/\1/p' | head -1)
|
||||
fi
|
||||
fi
|
||||
prefix="${exec_name:-$nospace_name}"
|
||||
[[ -z "$prefix" || ${#prefix} -lt 3 ]] && return 0
|
||||
|
||||
local dir_abs
|
||||
dir_abs=$(cd "$directory" 2> /dev/null && pwd -P 2> /dev/null) || return 0
|
||||
while IFS= read -r -d '' f; do
|
||||
[[ -z "$f" ]] && continue
|
||||
local base
|
||||
base=$(basename "$f" 2> /dev/null)
|
||||
[[ "$base" != "$prefix"* ]] && continue
|
||||
case "$base" in
|
||||
*.ips | *.crash | *.spin) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
printf '%s\n' "$f"
|
||||
done < <(find "$dir_abs" -maxdepth 1 -type f -name "${prefix}*" -print0 2> /dev/null || true)
|
||||
return 0
|
||||
}
|
||||
|
||||
# Locate system-level application files
|
||||
find_app_system_files() {
|
||||
local bundle_id="$1"
|
||||
|
||||
@@ -258,20 +258,28 @@ batch_uninstall_applications() {
|
||||
needs_sudo=true
|
||||
fi
|
||||
|
||||
# Size estimate includes related and system files.
|
||||
local app_size_kb=$(get_path_size_kb "$app_path" || echo "0")
|
||||
local related_files=$(find_app_files "$bundle_id" "$app_name" || true)
|
||||
local diag_user
|
||||
diag_user=$(get_diagnostic_report_paths_for_app "$app_path" "$app_name" "$HOME/Library/Logs/DiagnosticReports" || true)
|
||||
[[ -n "$diag_user" ]] && related_files=$(
|
||||
[[ -n "$related_files" ]] && echo "$related_files"
|
||||
echo "$diag_user"
|
||||
)
|
||||
local related_size_kb=$(calculate_total_size "$related_files" || echo "0")
|
||||
# system_files is a newline-separated string, not an array.
|
||||
# shellcheck disable=SC2178,SC2128
|
||||
local system_files=$(find_app_system_files "$bundle_id" "$app_name" || true)
|
||||
local diag_system
|
||||
diag_system=$(get_diagnostic_report_paths_for_app "$app_path" "$app_name" "/Library/Logs/DiagnosticReports" || true)
|
||||
# shellcheck disable=SC2128
|
||||
local system_size_kb=$(calculate_total_size "$system_files" || echo "0")
|
||||
local total_kb=$((app_size_kb + related_size_kb + system_size_kb))
|
||||
local diag_system_size_kb=$(calculate_total_size "$diag_system" || echo "0")
|
||||
local total_kb=$((app_size_kb + related_size_kb + system_size_kb + diag_system_size_kb))
|
||||
((total_estimated_size += total_kb)) || true
|
||||
|
||||
# shellcheck disable=SC2128
|
||||
if [[ -n "$system_files" ]]; then
|
||||
if [[ -n "$system_files" || -n "$diag_system" ]]; then
|
||||
needs_sudo=true
|
||||
fi
|
||||
|
||||
@@ -290,7 +298,9 @@ batch_uninstall_applications() {
|
||||
encoded_files=$(printf '%s' "$related_files" | base64 | tr -d '\n' || echo "")
|
||||
local encoded_system_files
|
||||
encoded_system_files=$(printf '%s' "$system_files" | base64 | tr -d '\n' || echo "")
|
||||
app_details+=("$app_name|$app_path|$bundle_id|$total_kb|$encoded_files|$encoded_system_files|$has_sensitive_data|$needs_sudo|$is_brew_cask|$cask_name")
|
||||
local encoded_diag_system
|
||||
encoded_diag_system=$(printf '%s' "$diag_system" | base64 | tr -d '\n' || echo "")
|
||||
app_details+=("$app_name|$app_path|$bundle_id|$total_kb|$encoded_files|$encoded_system_files|$has_sensitive_data|$needs_sudo|$is_brew_cask|$cask_name|$encoded_diag_system")
|
||||
done
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
|
||||
@@ -313,7 +323,7 @@ batch_uninstall_applications() {
|
||||
fi
|
||||
|
||||
for detail in "${app_details[@]}"; do
|
||||
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo_flag is_brew_cask cask_name <<< "$detail"
|
||||
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo_flag is_brew_cask cask_name encoded_diag_system <<< "$detail"
|
||||
local app_size_display=$(bytes_to_human "$((total_kb * 1024))")
|
||||
|
||||
local brew_tag=""
|
||||
@@ -323,6 +333,12 @@ batch_uninstall_applications() {
|
||||
# Show detailed file list for ALL apps (brew casks leave user data behind)
|
||||
local related_files=$(decode_file_list "$encoded_files" "$app_name")
|
||||
local system_files=$(decode_file_list "$encoded_system_files" "$app_name")
|
||||
local diag_system_display
|
||||
diag_system_display=$(decode_file_list "$encoded_diag_system" "$app_name")
|
||||
[[ -n "$diag_system_display" ]] && system_files=$(
|
||||
[[ -n "$system_files" ]] && echo "$system_files"
|
||||
echo "$diag_system_display"
|
||||
)
|
||||
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} ${app_path/$HOME/~}"
|
||||
|
||||
@@ -409,9 +425,10 @@ batch_uninstall_applications() {
|
||||
local current_index=0
|
||||
for detail in "${app_details[@]}"; do
|
||||
((current_index++))
|
||||
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo is_brew_cask cask_name <<< "$detail"
|
||||
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo is_brew_cask cask_name encoded_diag_system <<< "$detail"
|
||||
local related_files=$(decode_file_list "$encoded_files" "$app_name")
|
||||
local system_files=$(decode_file_list "$encoded_system_files" "$app_name")
|
||||
local diag_system=$(decode_file_list "$encoded_diag_system" "$app_name")
|
||||
local reason=""
|
||||
local suggestion=""
|
||||
|
||||
@@ -511,11 +528,17 @@ batch_uninstall_applications() {
|
||||
if [[ -z "$reason" ]]; then
|
||||
remove_file_list "$related_files" "false" > /dev/null
|
||||
|
||||
# If brew successfully uninstalled the cask, avoid deleting
|
||||
# system-level files Mole discovered. Brew manages its own
|
||||
# receipts/symlinks and we don't want to fight it.
|
||||
if [[ "$used_brew_successfully" != "true" ]]; then
|
||||
remove_file_list "$system_files" "true" > /dev/null
|
||||
if [[ "$used_brew_successfully" == "true" ]]; then
|
||||
remove_file_list "$diag_system" "true" > /dev/null
|
||||
else
|
||||
local system_all="$system_files"
|
||||
if [[ -n "$diag_system" ]]; then
|
||||
if [[ -n "$system_all" ]]; then
|
||||
system_all+=$'\n'
|
||||
fi
|
||||
system_all+="$diag_system"
|
||||
fi
|
||||
remove_file_list "$system_all" "true" > /dev/null
|
||||
fi
|
||||
|
||||
# Clean up macOS defaults (preference domains).
|
||||
|
||||
122
tests/test_diagnostic_reports_standalone.sh
Normal file
122
tests/test_diagnostic_reports_standalone.sh
Normal file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
# Standalone test for get_diagnostic_report_paths_for_app (Issue #441). Run: bash tests/test_diagnostic_reports_standalone.sh
|
||||
|
||||
set +e
|
||||
set +u
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
if [[ ! -f "$PROJECT_ROOT/lib/core/app_protection.sh" ]]; then
|
||||
PROJECT_ROOT="$(pwd)"
|
||||
SCRIPT_DIR="$PROJECT_ROOT/tests"
|
||||
fi
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
source_crlf_safe() {
|
||||
local f="$1"
|
||||
if [[ -f "$f" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source /dev/stdin <<< "$(sed 's/\r$//' < "$f")"
|
||||
fi
|
||||
}
|
||||
|
||||
source_crlf_safe "$PROJECT_ROOT/lib/core/base.sh"
|
||||
source_crlf_safe "$PROJECT_ROOT/lib/core/app_protection.sh"
|
||||
set +e
|
||||
set +u
|
||||
|
||||
FAILED=0
|
||||
PASSED=0
|
||||
|
||||
assert_contains() {
|
||||
local haystack="$1"
|
||||
local needle="$2"
|
||||
local name="${3:-assert}"
|
||||
if [[ "$haystack" == *"$needle"* ]]; then
|
||||
echo " OK $name"
|
||||
((PASSED++))
|
||||
return 0
|
||||
fi
|
||||
echo " FAIL $name (expected to find: $needle)"
|
||||
((FAILED++))
|
||||
return 1
|
||||
}
|
||||
|
||||
assert_empty() {
|
||||
local val="$1"
|
||||
local name="${2:-assert}"
|
||||
if [[ -z "$val" ]]; then
|
||||
echo " OK $name (empty as expected)"
|
||||
((PASSED++))
|
||||
return 0
|
||||
fi
|
||||
echo " FAIL $name (expected empty, got: $val)"
|
||||
((FAILED++))
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "Testing get_diagnostic_report_paths_for_app (DiagnosticReports uninstall)"
|
||||
echo ""
|
||||
|
||||
out=$(get_diagnostic_report_paths_for_app "/Applications/Foo.app" "Foo" "/nonexistent/dir" 2> /dev/null || true)
|
||||
assert_empty "$out" "missing directory returns empty"
|
||||
|
||||
TMP_EMPTY=$(mktemp -d 2> /dev/null || mktemp -d -t mole-test 2> /dev/null || echo "")
|
||||
[[ -z "$TMP_EMPTY" ]] && TMP_EMPTY="/tmp/mole-test-$$" && mkdir -p "$TMP_EMPTY"
|
||||
out=$(get_diagnostic_report_paths_for_app "" "Ab" "$TMP_EMPTY" 2> /dev/null || true)
|
||||
assert_empty "$out" "empty app_path returns empty"
|
||||
rm -rf "$TMP_EMPTY" 2> /dev/null || true
|
||||
|
||||
TMP_DIAG=$(mktemp -d 2> /dev/null || mktemp -d -t mole-diag 2> /dev/null || echo "/tmp/mole-diag-$$")
|
||||
TMP_APP=$(mktemp -d 2> /dev/null || mktemp -d -t mole-app 2> /dev/null || echo "/tmp/mole-app-$$")
|
||||
mkdir -p "$TMP_DIAG" "$TMP_APP"
|
||||
mkdir -p "$TMP_APP/Contents"
|
||||
printf '%s' '<?xml version="1.0"?><plist version="1.0"><dict><key>CFBundleExecutable</key><string>MyApp</string></dict></plist>' > "$TMP_APP/Contents/Info.plist"
|
||||
|
||||
touch "$TMP_DIAG/MyApp_2025-02-10-120000_host.ips"
|
||||
touch "$TMP_DIAG/MyApp.crash"
|
||||
touch "$TMP_DIAG/MyApp_2025-02-10-120001_host.spin"
|
||||
touch "$TMP_DIAG/OtherApp_2025-02-10.ips"
|
||||
touch "$TMP_DIAG/MyApp_log.txt"
|
||||
|
||||
out=$(get_diagnostic_report_paths_for_app "$TMP_APP" "My App" "$TMP_DIAG" 2> /dev/null || true)
|
||||
|
||||
assert_contains "$out" "MyApp_2025-02-10-120000" "returns .ips file"
|
||||
assert_contains "$out" "MyApp.crash" "returns .crash file"
|
||||
assert_contains "$out" "MyApp_2025-02-10-120001" "returns .spin file"
|
||||
assert_contains "$out" ".ips" "output contains .ips path"
|
||||
if [[ "$out" == *"OtherApp"* ]]; then
|
||||
echo " FAIL should not return OtherApp"
|
||||
((FAILED++))
|
||||
else
|
||||
echo " OK does not return OtherApp"
|
||||
((PASSED++))
|
||||
fi
|
||||
if [[ "$out" == *"MyApp_log.txt"* ]]; then
|
||||
echo " FAIL should not return non-diagnostic extension"
|
||||
((FAILED++))
|
||||
else
|
||||
echo " OK does not return .txt file"
|
||||
((PASSED++))
|
||||
fi
|
||||
|
||||
rm -rf "$TMP_DIAG" "$TMP_APP" 2> /dev/null || true
|
||||
|
||||
TMP_DIAG2=$(mktemp -d 2> /dev/null || mktemp -d -t mole-diag2 2> /dev/null || echo "/tmp/mole-diag2-$$")
|
||||
TMP_APP2=$(mktemp -d 2> /dev/null || mktemp -d -t mole-app2 2> /dev/null || echo "/tmp/mole-app2-$$")
|
||||
mkdir -p "$TMP_DIAG2" "$TMP_APP2"
|
||||
mkdir -p "$TMP_APP2/Contents"
|
||||
touch "$TMP_DIAG2/TestApp_2025-02-10.ips"
|
||||
|
||||
out=$(get_diagnostic_report_paths_for_app "$TMP_APP2" "Test App" "$TMP_DIAG2" 2> /dev/null || true)
|
||||
assert_contains "$out" "TestApp_" "fallback to nospace app name matches file"
|
||||
|
||||
rm -rf "$TMP_DIAG2" "$TMP_APP2" 2> /dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "Result: $PASSED passed, $FAILED failed"
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "All DiagnosticReports tests passed."
|
||||
exit 0
|
||||
Reference in New Issue
Block a user