mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 15:04:42 +00:00
feat: enhance uninstall with launch items and login items cleanup
- Add automatic cleanup of LaunchAgents/Daemons (Issue #315) - Support both system and user-level launch paths - Add Login Items cleanup (fixing broken entries like CodexBar) - Improve Homebrew uninstall logging visibility - Update security audit and tests
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
**Status:** PASSED | **Risk Level:** LOW | **Version:** 1.19.0 (2026-01-09)
|
**Status:** PASSED | **Risk Level:** LOW | **Version:** 1.21.0 (2026-01-15)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -12,9 +12,9 @@
|
|||||||
|
|
||||||
| Attribute | Details |
|
| Attribute | Details |
|
||||||
|-----------|---------|
|
|-----------|---------|
|
||||||
| Audit Date | January 9, 2026 |
|
| Audit Date | January 15, 2026 |
|
||||||
| Audit Conclusion | **PASSED** |
|
| Audit Conclusion | **PASSED** |
|
||||||
| Mole Version | V1.19.0 |
|
| Mole Version | V1.21.0 |
|
||||||
| Audited Branch | `main` (HEAD) |
|
| Audited Branch | `main` (HEAD) |
|
||||||
| Scope | Shell scripts, Go binaries, Configuration |
|
| Scope | Shell scripts, Go binaries, Configuration |
|
||||||
| Methodology | Static analysis, Threat modeling, Code review |
|
| Methodology | Static analysis, Threat modeling, Code review |
|
||||||
@@ -176,18 +176,18 @@ For user-selected app removal:
|
|||||||
| AI & LLM Tools | Cursor, Claude, ChatGPT, Ollama, LM Studio | Protects models, tokens, and sessions |
|
| AI & LLM Tools | Cursor, Claude, ChatGPT, Ollama, LM Studio | Protects models, tokens, and sessions |
|
||||||
| Startup Items | `com.apple.*` LaunchAgents/Daemons | System items unconditionally skipped |
|
| Startup Items | `com.apple.*` LaunchAgents/Daemons | System items unconditionally skipped |
|
||||||
|
|
||||||
**Orphaned Helper Cleanup (`opt_startup_items_cleanup`):**
|
**LaunchAgent/LaunchDaemon Cleanup During Uninstallation:**
|
||||||
|
|
||||||
Removes LaunchAgents/Daemons whose associated app has been uninstalled:
|
When users uninstall applications via `mo uninstall`, Mole automatically removes associated LaunchAgent and LaunchDaemon plists:
|
||||||
|
|
||||||
- Checks `AssociatedBundleIdentifiers` to detect orphans.
|
- Scans `~/Library/LaunchAgents`, `~/Library/LaunchDaemons`, `/Library/LaunchAgents`, `/Library/LaunchDaemons`
|
||||||
- Skips all `com.apple.*` system items.
|
- Matches both exact bundle ID (`com.example.app.plist`) and app name patterns (`*AppName*.plist`)
|
||||||
- Skips paths under `/System/*`, `/usr/bin/*`, `/usr/lib/*`, `/usr/sbin/*`, `/Library/Apple/*`.
|
- Skips all `com.apple.*` system items via `should_protect_path()` validation
|
||||||
- Uses `safe_remove` / `safe_sudo_remove` with path validation.
|
- Unloads services via `launchctl` before deletion (via `stop_launch_services()`)
|
||||||
- Unloads service via `launchctl` before deletion.
|
- **Safer than orphan detection:** Only removes plists when the associated app is explicitly being uninstalled
|
||||||
- **Timeout Protection:** 10-second limit on `mdfind` operations.
|
- Prevents accumulation of orphaned startup items that persist after app removal
|
||||||
|
|
||||||
**Code:** `lib/optimize/tasks.sh:opt_startup_items_cleanup()`
|
**Code:** `lib/core/app_protection.sh:find_app_files()`, `lib/uninstall/batch.sh:stop_launch_services()`
|
||||||
|
|
||||||
### Crash Safety & Atomic Operations
|
### Crash Safety & Atomic Operations
|
||||||
|
|
||||||
|
|||||||
@@ -660,6 +660,7 @@ find_app_files() {
|
|||||||
"$HOME/Library/HTTPStorages/$bundle_id"
|
"$HOME/Library/HTTPStorages/$bundle_id"
|
||||||
"$HOME/Library/Cookies/$bundle_id.binarycookies"
|
"$HOME/Library/Cookies/$bundle_id.binarycookies"
|
||||||
"$HOME/Library/LaunchAgents/$bundle_id.plist"
|
"$HOME/Library/LaunchAgents/$bundle_id.plist"
|
||||||
|
"$HOME/Library/LaunchDaemons/$bundle_id.plist"
|
||||||
"$HOME/Library/Application Scripts/$bundle_id"
|
"$HOME/Library/Application Scripts/$bundle_id"
|
||||||
"$HOME/Library/Services/$app_name.workflow"
|
"$HOME/Library/Services/$app_name.workflow"
|
||||||
"$HOME/Library/QuickLook/$app_name.qlgenerator"
|
"$HOME/Library/QuickLook/$app_name.qlgenerator"
|
||||||
@@ -734,11 +735,18 @@ find_app_files() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Launch Agents by name (special handling)
|
# Launch Agents and Daemons by name (special handling)
|
||||||
if [[ ${#app_name} -gt 3 ]] && [[ -d ~/Library/LaunchAgents ]]; then
|
if [[ ${#app_name} -gt 3 ]]; then
|
||||||
while IFS= read -r -d '' plist; do
|
if [[ -d ~/Library/LaunchAgents ]]; then
|
||||||
files_to_clean+=("$plist")
|
while IFS= read -r -d '' plist; do
|
||||||
done < <(command find ~/Library/LaunchAgents -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null)
|
files_to_clean+=("$plist")
|
||||||
|
done < <(command find ~/Library/LaunchAgents -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null)
|
||||||
|
fi
|
||||||
|
if [[ -d ~/Library/LaunchDaemons ]]; then
|
||||||
|
while IFS= read -r -d '' plist; do
|
||||||
|
files_to_clean+=("$plist")
|
||||||
|
done < <(command find ~/Library/LaunchDaemons -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null)
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Handle specialized toolchains and development environments
|
# Handle specialized toolchains and development environments
|
||||||
|
|||||||
@@ -88,6 +88,40 @@ stop_launch_services() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Remove macOS Login Items for an app
|
||||||
|
remove_login_item() {
|
||||||
|
local app_name="$1"
|
||||||
|
local bundle_id="$2"
|
||||||
|
|
||||||
|
# Skip if no identifiers provided
|
||||||
|
[[ -z "$app_name" && -z "$bundle_id" ]] && return 0
|
||||||
|
|
||||||
|
# Strip .app suffix if present (login items don't include it)
|
||||||
|
local clean_name="${app_name%.app}"
|
||||||
|
|
||||||
|
# Remove from Login Items using index-based deletion (handles broken items)
|
||||||
|
if [[ -n "$clean_name" ]]; then
|
||||||
|
osascript <<-EOF 2>/dev/null || true
|
||||||
|
tell application "System Events"
|
||||||
|
try
|
||||||
|
set itemCount to count of login items
|
||||||
|
-- Delete in reverse order to avoid index shifting
|
||||||
|
repeat with i from itemCount to 1 by -1
|
||||||
|
try
|
||||||
|
set itemName to name of login item i
|
||||||
|
if itemName is "$clean_name" then
|
||||||
|
delete login item i
|
||||||
|
end if
|
||||||
|
end try
|
||||||
|
end repeat
|
||||||
|
end try
|
||||||
|
end tell
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Remove files (handles symlinks, optional sudo).
|
# Remove files (handles symlinks, optional sudo).
|
||||||
remove_file_list() {
|
remove_file_list() {
|
||||||
local file_list="$1"
|
local file_list="$1"
|
||||||
@@ -364,6 +398,9 @@ batch_uninstall_applications() {
|
|||||||
[[ -n "$system_files" ]] && has_system_files="true"
|
[[ -n "$system_files" ]] && has_system_files="true"
|
||||||
stop_launch_services "$bundle_id" "$has_system_files"
|
stop_launch_services "$bundle_id" "$has_system_files"
|
||||||
|
|
||||||
|
# Remove from Login Items
|
||||||
|
remove_login_item "$app_name" "$bundle_id"
|
||||||
|
|
||||||
if ! force_kill_app "$app_name" "$app_path"; then
|
if ! force_kill_app "$app_name" "$app_path"; then
|
||||||
reason="still running"
|
reason="still running"
|
||||||
fi
|
fi
|
||||||
@@ -371,15 +408,20 @@ batch_uninstall_applications() {
|
|||||||
# Remove the application only if not running.
|
# Remove the application only if not running.
|
||||||
if [[ -z "$reason" ]]; then
|
if [[ -z "$reason" ]]; then
|
||||||
if [[ "$is_brew_cask" == "true" && -n "$cask_name" ]]; then
|
if [[ "$is_brew_cask" == "true" && -n "$cask_name" ]]; then
|
||||||
# Use brew uninstall --cask with progress indicator
|
# Stop spinner before brew output
|
||||||
local brew_output_file=$(mktemp)
|
if [[ -t 1 ]]; then
|
||||||
|
stop_inline_spinner
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use brew uninstall --cask - show output directly
|
||||||
local brew_failed=false
|
local brew_failed=false
|
||||||
if ! run_with_timeout 120 brew uninstall --cask "$cask_name" > "$brew_output_file" 2>&1; then
|
if ! run_with_timeout 120 brew uninstall --cask "$cask_name" 2>&1; then
|
||||||
brew_failed=true
|
brew_failed=true
|
||||||
log_warning "brew uninstall failed for $app_name, falling back to manual cleanup"
|
log_warning "brew uninstall failed for $app_name, falling back to manual cleanup"
|
||||||
fi
|
fi
|
||||||
rm -f "$brew_output_file"
|
|
||||||
if [[ "$brew_failed" == "true" ]]; then
|
if [[ "$brew_failed" == "true" ]]; then
|
||||||
|
# Fallback to manual cleanup
|
||||||
[[ -z "$related_files" ]] && related_files=$(find_app_files "$bundle_id" "$app_name")
|
[[ -z "$related_files" ]] && related_files=$(find_app_files "$bundle_id" "$app_name")
|
||||||
[[ -z "$system_files" ]] && system_files=$(find_app_system_files "$bundle_id" "$app_name")
|
[[ -z "$system_files" ]] && system_files=$(find_app_system_files "$bundle_id" "$app_name")
|
||||||
if [[ "$needs_sudo" == true ]]; then
|
if [[ "$needs_sudo" == true ]]; then
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ create_app_artifacts() {
|
|||||||
mkdir -p "$HOME/Library/Preferences/ByHost"
|
mkdir -p "$HOME/Library/Preferences/ByHost"
|
||||||
touch "$HOME/Library/Preferences/ByHost/com.example.TestApp.ABC123.plist"
|
touch "$HOME/Library/Preferences/ByHost/com.example.TestApp.ABC123.plist"
|
||||||
mkdir -p "$HOME/Library/Saved Application State/com.example.TestApp.savedState"
|
mkdir -p "$HOME/Library/Saved Application State/com.example.TestApp.savedState"
|
||||||
|
mkdir -p "$HOME/Library/LaunchAgents"
|
||||||
|
touch "$HOME/Library/LaunchAgents/com.example.TestApp.plist"
|
||||||
|
mkdir -p "$HOME/Library/LaunchDaemons"
|
||||||
|
touch "$HOME/Library/LaunchDaemons/com.example.TestApp.plist"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "find_app_files discovers user-level leftovers" {
|
@test "find_app_files discovers user-level leftovers" {
|
||||||
@@ -55,6 +59,8 @@ EOF
|
|||||||
[[ "$result" == *"Preferences/com.example.TestApp.plist"* ]]
|
[[ "$result" == *"Preferences/com.example.TestApp.plist"* ]]
|
||||||
[[ "$result" == *"Saved Application State/com.example.TestApp.savedState"* ]]
|
[[ "$result" == *"Saved Application State/com.example.TestApp.savedState"* ]]
|
||||||
[[ "$result" == *"Containers/com.example.TestApp"* ]]
|
[[ "$result" == *"Containers/com.example.TestApp"* ]]
|
||||||
|
[[ "$result" == *"LaunchAgents/com.example.TestApp.plist"* ]]
|
||||||
|
[[ "$result" == *"LaunchDaemons/com.example.TestApp.plist"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "calculate_total_size returns aggregate kilobytes" {
|
@test "calculate_total_size returns aggregate kilobytes" {
|
||||||
@@ -114,6 +120,8 @@ batch_uninstall_applications
|
|||||||
[[ ! -d "$HOME/Library/Application Support/TestApp" ]] || exit 1
|
[[ ! -d "$HOME/Library/Application Support/TestApp" ]] || exit 1
|
||||||
[[ ! -d "$HOME/Library/Caches/TestApp" ]] || exit 1
|
[[ ! -d "$HOME/Library/Caches/TestApp" ]] || exit 1
|
||||||
[[ ! -f "$HOME/Library/Preferences/com.example.TestApp.plist" ]] || exit 1
|
[[ ! -f "$HOME/Library/Preferences/com.example.TestApp.plist" ]] || exit 1
|
||||||
|
[[ ! -f "$HOME/Library/LaunchAgents/com.example.TestApp.plist" ]] || exit 1
|
||||||
|
[[ ! -f "$HOME/Library/LaunchDaemons/com.example.TestApp.plist" ]] || exit 1
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
|
|||||||
Reference in New Issue
Block a user