From 0597021fd426512513fd0007fa12aefc97f1d064 Mon Sep 17 00:00:00 2001 From: tw93 Date: Sat, 21 Feb 2026 20:21:54 +0800 Subject: [PATCH] fix(purge): save and restore caller traps to prevent state leak - Save caller's INT/TERM traps before installing local cleanup trap - Restore original traps after clean_project_artifacts completes - Add test to verify trap restoration behavior Fixes P3 issue: project.sh (line 825, 870) --- lib/clean/project.sh | 22 ++++++++++++++-------- tests/purge.bats | 30 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/lib/clean/project.sh b/lib/clean/project.sh index a3bda4c..db5bf8f 100644 --- a/lib/clean/project.sh +++ b/lib/clean/project.sh @@ -800,6 +800,9 @@ clean_project_artifacts() { local -a all_found_items=() local -a safe_to_clean=() local -a recently_modified=() + local previous_int_trap="" + local previous_term_trap="" + local trap_installed_by_this_call=false # Set up cleanup on interrupt # Note: Declared without 'local' so cleanup_scan trap can access them scan_pids=() @@ -820,12 +823,11 @@ clean_project_artifacts() { echo "" exit 130 } - # Set up signal handling only if not already set by parent script - # This prevents trap conflicts between purge.sh and project.sh - if [[ -z "${_MO_PURGE_TRAP_SET:-}" ]]; then - trap cleanup_scan INT TERM - export _MO_PURGE_TRAP_SET=1 - fi + # Save caller traps and install local cleanup trap for this function call. + previous_int_trap=$(trap -p INT || true) + previous_term_trap=$(trap -p TERM || true) + trap cleanup_scan INT TERM + trap_installed_by_this_call=true # Scanning is started from purge.sh with start_inline_spinner # Launch all scans in parallel for path in "${PURGE_SEARCH_PATHS[@]}"; do @@ -866,8 +868,12 @@ clean_project_artifacts() { rm -f "$scan_output" fi done - # Clean up trap - trap - INT TERM + # Restore caller traps after this function completes. + if [[ "$trap_installed_by_this_call" == "true" ]]; then + trap - INT TERM + [[ -n "$previous_int_trap" ]] && eval "$previous_int_trap" + [[ -n "$previous_term_trap" ]] && eval "$previous_term_trap" + fi if [[ ${#all_found_items[@]} -eq 0 ]]; then echo "" echo -e "${GREEN}${ICON_SUCCESS}${NC} Great! No old project artifacts to clean" diff --git a/tests/purge.bats b/tests/purge.bats index 654d69f..31cf904 100644 --- a/tests/purge.bats +++ b/tests/purge.bats @@ -505,6 +505,36 @@ EOF [[ "$result" == "TIMEOUT" ]] } +@test "clean_project_artifacts: restores caller INT/TERM traps" { + result=$(bash -c " + set -euo pipefail + export HOME='$HOME' + source '$PROJECT_ROOT/lib/core/common.sh' + source '$PROJECT_ROOT/lib/clean/project.sh' + mkdir -p '$HOME/www' + PURGE_SEARCH_PATHS=('$HOME/www') + trap 'echo parent-int' INT + trap 'echo parent-term' TERM + before_int=\$(trap -p INT) + before_term=\$(trap -p TERM) + clean_project_artifacts > /dev/null 2>&1 || true + after_int=\$(trap -p INT) + after_term=\$(trap -p TERM) + if [[ \"\$before_int\" == \"\$after_int\" && \"\$before_term\" == \"\$after_term\" ]]; then + echo 'PASS' + else + echo 'FAIL' + echo \"before_int=\$before_int\" + echo \"after_int=\$after_int\" + echo \"before_term=\$before_term\" + echo \"after_term=\$after_term\" + exit 1 + fi + ") + + [[ "$result" == *"PASS"* ]] +} + @test "clean_project_artifacts: handles empty directory gracefully" { run bash -c " export HOME='$HOME'