diff --git a/install.sh b/install.sh index f81ac7f45..24b139420 100755 --- a/install.sh +++ b/install.sh @@ -66,6 +66,82 @@ else C_GREEN='' C_BOLD='' C_DIM='' C_RED='' C_YELLOW='' C_CYAN='' C_RESET='' fi +# --------------------------------------------------------------------------- +# Cleanup — kill backgrounded processes and remove temp files on exit/signal. +# --------------------------------------------------------------------------- +_cleanup_pids=() +_cleanup_files=() + +remove_tracked_pid() { + local target="$1" + local kept=() + local value + if (( ${#_cleanup_pids[@]} == 0 )); then + return + fi + for value in "${_cleanup_pids[@]}"; do + if [[ "$value" != "$target" ]]; then + kept+=("$value") + fi + done + if (( ${#kept[@]} )); then + _cleanup_pids=("${kept[@]}") + else + _cleanup_pids=() + fi +} + +remove_tracked_file() { + local target="$1" + local kept=() + local value + if (( ${#_cleanup_files[@]} == 0 )); then + return + fi + for value in "${_cleanup_files[@]}"; do + if [[ "$value" != "$target" ]]; then + kept+=("$value") + fi + done + if (( ${#kept[@]} )); then + _cleanup_files=("${kept[@]}") + else + _cleanup_files=() + fi +} + +cleanup() { + local rc=$? + set +e + if (( ${#_cleanup_pids[@]} )); then + for pid in "${_cleanup_pids[@]}"; do + kill "$pid" 2>/dev/null || true + done + fi + if (( ${#_cleanup_files[@]} )); then + for f in "${_cleanup_files[@]}"; do + rm -rf "$f" + done + fi + return "$rc" +} + +# Handle signals by running cleanup and then re-raising so the parent sees +# the correct signal exit status instead of silently continuing. +trap_signal() { + local sig="$1" + cleanup + trap - "$sig" + kill -s "$sig" "$$" +} + +register_cleanup_traps() { + trap cleanup EXIT + trap 'trap_signal INT' INT + trap 'trap_signal TERM' TERM + trap 'trap_signal HUP' HUP +} + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -206,8 +282,10 @@ spin() { local log log=$(mktemp) + _cleanup_files+=("$log") "$@" >"$log" 2>&1 & local pid=$! i=0 + _cleanup_pids+=("$pid") local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') while kill -0 "$pid" 2>/dev/null; do @@ -228,6 +306,9 @@ spin() { printf "\n" fi rm -f "$log" + # Untrack resources so cleanup() does not act on stale/recycled entries. + remove_tracked_pid "$pid" + remove_tracked_file "$log" return $status } @@ -372,9 +453,11 @@ install_nodejs() { local NVM_SHA256="4b7412c49960c7d31e8df72da90c1fb5b8cccb419ac99537b737028d497aba4f" local nvm_tmp nvm_tmp="$(mktemp)" + _cleanup_files+=("$nvm_tmp") curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" -o "$nvm_tmp" \ || { rm -f "$nvm_tmp" + remove_tracked_file "$nvm_tmp" error "Failed to download nvm installer" } local actual_hash @@ -388,11 +471,13 @@ install_nodejs() { fi if [[ "$actual_hash" != "$NVM_SHA256" ]]; then rm -f "$nvm_tmp" + remove_tracked_file "$nvm_tmp" error "nvm installer integrity check failed\n Expected: $NVM_SHA256\n Actual: $actual_hash" fi info "nvm installer integrity verified" spin "Installing nvm..." bash "$nvm_tmp" rm -f "$nvm_tmp" + remove_tracked_file "$nvm_tmp" ensure_nvm_loaded spin "Installing Node.js 22..." bash -c ". \"$NVM_DIR/nvm.sh\" && nvm install 22 --no-progress" ensure_nvm_loaded @@ -494,6 +579,10 @@ pre_extract_openclaw() { info "Pre-extracting openclaw@${openclaw_version} with system tar (GH-503 workaround)…" local tmpdir tmpdir="$(mktemp -d)" + trap 'rm -rf "$tmpdir"' EXIT INT TERM HUP + # Not tracked in _cleanup_files: this function runs in a child shell via + # spin()/bash -c, so array mutations do not reach the parent. The child + # cleans up tmpdir itself; on interrupt, spin() kills the child PID. if npm pack "openclaw@${openclaw_version}" --pack-destination "$tmpdir" >/dev/null 2>&1; then local tgz tgz="$(find "$tmpdir" -maxdepth 1 -name 'openclaw-*.tgz' -print -quit)" @@ -666,6 +755,7 @@ post_install_message() { # Main # --------------------------------------------------------------------------- main() { + register_cleanup_traps # Parse flags NON_INTERACTIVE="" for arg in "$@"; do diff --git a/test/install-preflight.test.js b/test/install-preflight.test.js index e24fe17e4..bbd39b124 100644 --- a/test/install-preflight.test.js +++ b/test/install-preflight.test.js @@ -1101,6 +1101,25 @@ describe("installer pure helpers", () => { ); } + it("does not install cleanup traps when sourced", () => { + const r = spawnSync( + "bash", + [ + "-c", + `source "${INSTALLER}" 2>/dev/null; [[ -z "$(trap -p EXIT)" && -z "$(trap -p INT)" && -z "$(trap -p TERM)" && -z "$(trap -p HUP)" ]] && echo clean`, + ], + { + cwd: path.join(import.meta.dirname, ".."), + encoding: "utf-8", + env: { + HOME: os.tmpdir(), + PATH: TEST_SYSTEM_PATH, + }, + }, + ); + expect(r.stdout.trim()).toBe("clean"); + }); + // -- version_gte -- it("version_gte: equal versions return 0", () => {