Skip to content

fix(install): clean up temp files and child processes on exit#1026

Open
stevenobiajulu wants to merge 4 commits intoNVIDIA:mainfrom
stevenobiajulu:fix/install-trap-handler
Open

fix(install): clean up temp files and child processes on exit#1026
stevenobiajulu wants to merge 4 commits intoNVIDIA:mainfrom
stevenobiajulu:fix/install-trap-handler

Conversation

@stevenobiajulu
Copy link
Copy Markdown

@stevenobiajulu stevenobiajulu commented Mar 27, 2026

Summary

Fixes #1020 by cleaning up tracked background processes and temp files when install.sh exits or is interrupted.

  • add cleanup() plus signal handlers that re-raise INT / TERM / HUP after cleanup
  • track spinner PIDs/temp files and the install_nodejs() temp installer, then untrack exact matches after normal completion
  • keep bash 3.2 / set -u compatibility via length-guarded array handling

Test plan

  • Full unit test suite passes locally (542/542, 2 pre-existing skips)
  • install-preflight.test.js passes — these tests source install.sh and call functions in isolation, exercising the cleanup trap on subshell exit with empty arrays
  • cli.test.js passes (21/21)
  • Ran make check locally; observed pre-existing local-tooling failures (typescript-eslint module resolution and missing hadolint) unrelated to this shell-only patch
  • Ran installer-focused tests in Docker (node:22-slim); the same pre-existing test/cli.test.js gateway failure reproduces on upstream/main, so it is not introduced by this patch
  • Confirmed bash 3.2 compatibility — empty tracked arrays do not trip set -u during trap cleanup

Summary by CodeRabbit

  • Bug Fixes

    • Installer now reliably cleans up background processes and temporary files on errors, interrupts, or exit; per-invocation temp directories are removed on interruption to avoid stale artifacts.
  • Tests

    • Added a test ensuring sourcing the installer does not register persistent signal handlers.
  • Documentation

    • Clarified interrupt and temporary-directory behavior in installer comments.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

Added signal-aware cleanup to the installer: track background PIDs and temp files, register traps (EXIT, INT, TERM, HUP) to run a cleanup routine that kills tracked PIDs and removes tracked files, integrate tracking into spinner and nodejs installer, and add a test ensuring sourcing doesn't leave traps.

Changes

Cohort / File(s) Summary
Installer script
install.sh
Add global arrays _cleanup_pids and _cleanup_files, mutators (remove_tracked_pid, remove_tracked_file), cleanup() routine, trap_signal()/register_cleanup_traps(), call registration in main(); update spin() and install_nodejs() to register/unregister temp files and PIDs; add local per-invocation trap in pre_extract_openclaw().
Tests
test/install-preflight.test.js
Add Vitest case that sources install.sh in a subshell and asserts no trap handlers are left for EXIT, INT, TERM, or HUP (expects clean output).

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Installer as install.sh
  participant Spinner
  participant Worker as BackgroundCmd
  participant Cleanup

  User->>Installer: run installer
  Installer->>Spinner: invoke spin("long command")
  Spinner->>Worker: spawn command (background) & create mktemp log
  Spinner->>Installer: register PID and file in cleanup lists
  User->>Installer: send SIGINT (Ctrl+C)
  Installer->>Cleanup: trap triggers cleanup()
  Cleanup->>Worker: kill tracked PIDs
  Cleanup->>Installer: remove tracked files
  Cleanup->>Installer: re-emit signal
  Installer->>User: script exits
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I’m a rabbit who hops with care,
I track each PID and temp file there,
When Ctrl+C rattles the install night,
I tidy logs and end the fight,
Hopping off — no ghosts in sight. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(install): clean up temp files and child processes on exit' directly and concisely describes the main change—implementing cleanup of temporary files and background processes when install.sh exits.
Linked Issues check ✅ Passed The PR implements all core requirements from issue #1020: cleanup function to kill tracked PIDs and remove tracked files, trap handlers for EXIT/SIGINT/SIGTERM, arrays to maintain tracked resources, and untracking after normal completion.
Out of Scope Changes check ✅ Passed All changes in install.sh and the test file are directly scoped to implementing cleanup traps and tracking mechanisms specified in issue #1020; no unrelated modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
install.sh (2)

397-420: Consider untracking temp file after explicit removal for consistency.

The temp file is tracked at line 399 but not untracked after explicit rm -f at lines 402, 415, and 420. This is harmless (rm -rf on non-existent files is a no-op) but inconsistent with spin() which untracks after removal.

♻️ Optional: untrack after explicit removal
     rm -f "$nvm_tmp"
+    remove_tracked_file "$nvm_tmp"
     error "Failed to download nvm installer"

Apply similar changes at lines 415-416 and after line 420.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` around lines 397 - 420, The temp file tracker _cleanup_files
currently gets the nvm_tmp entry but is not removed when the script explicitly
deletes the temp file; update the explicit removal sites that call rm -f
"$nvm_tmp" (the failure branch after curl, the integrity-fail branch, and the
normal cleanup after spin) to also remove nvm_tmp from _cleanup_files (e.g., by
filtering or using the same untrack helper used by spin) so the temporary path
is unregistered consistently whenever rm -f "$nvm_tmp" is performed; ensure you
modify the codepaths around the curl error handler, the integrity check failure
(where actual_hash != NVM_SHA256), and the final cleanup after spin to untrack
_cleanup_files alongside deleting the file.

123-134: Signal handling is correct; consider clearing arrays after cleanup to avoid redundant work.

The re-raise pattern is correct for propagating proper exit status to parent processes. However, when a signal is caught, cleanup() runs in trap_signal, and then the EXIT trap also invokes cleanup() again after the signal is re-raised. This is safe (idempotent) but wasteful.

♻️ Optional: clear arrays after cleanup to avoid double iteration
 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
+  _cleanup_pids=()
+  _cleanup_files=()
   return "$rc"
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` around lines 123 - 134, The cleanup function is being run twice
when a signal is handled (once in trap_signal and again via the EXIT trap after
re-raising), so to avoid redundant array iteration modify trap_signal to clear
any tracked arrays (or set a "cleaned" flag) immediately after calling cleanup;
specifically, update trap_signal (and/or cleanup) to reset the arrays you
iterate (e.g., unset or set to empty) or set a guard variable like
CLEANUP_DONE=true and check it at the top of cleanup to skip repeated work—use
the function names trap_signal and cleanup and ensure the EXIT trap behavior
remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@install.sh`:
- Around line 91-105: The function remove_tracked_file iterates over
_cleanup_files without guarding for an empty/unset array which triggers a Bash
3.2 + set -u failure; update remove_tracked_file to check the length of
_cleanup_files before the for-loop (e.g., if (( ${`#_cleanup_files`[@]} )); then
for value in "${_cleanup_files[@]}"; do ...; done; fi) so the loop is skipped
when the array is empty or unset, leaving the rest of the function (building
kept and reassigning _cleanup_files) unchanged.
- Around line 75-89: The remove_tracked_pid function iterates over
"${_cleanup_pids[@]}" without checking if the array is set, which under set -u
on Bash 3.2 causes an unbound variable error; modify remove_tracked_pid to first
test the array length (e.g., if (( ${`#_cleanup_pids`[@]} )); then ... fi) before
running the for loop that references _cleanup_pids, keeping the existing logic
that rebuilds _cleanup_pids (kept, _cleanup_pids=("${kept[@]}") or
_cleanup_pids=()) and refer to the function name remove_tracked_pid and the
variable _cleanup_pids when making the change.

---

Nitpick comments:
In `@install.sh`:
- Around line 397-420: The temp file tracker _cleanup_files currently gets the
nvm_tmp entry but is not removed when the script explicitly deletes the temp
file; update the explicit removal sites that call rm -f "$nvm_tmp" (the failure
branch after curl, the integrity-fail branch, and the normal cleanup after spin)
to also remove nvm_tmp from _cleanup_files (e.g., by filtering or using the same
untrack helper used by spin) so the temporary path is unregistered consistently
whenever rm -f "$nvm_tmp" is performed; ensure you modify the codepaths around
the curl error handler, the integrity check failure (where actual_hash !=
NVM_SHA256), and the final cleanup after spin to untrack _cleanup_files
alongside deleting the file.
- Around line 123-134: The cleanup function is being run twice when a signal is
handled (once in trap_signal and again via the EXIT trap after re-raising), so
to avoid redundant array iteration modify trap_signal to clear any tracked
arrays (or set a "cleaned" flag) immediately after calling cleanup;
specifically, update trap_signal (and/or cleanup) to reset the arrays you
iterate (e.g., unset or set to empty) or set a guard variable like
CLEANUP_DONE=true and check it at the top of cleanup to skip repeated work—use
the function names trap_signal and cleanup and ensure the EXIT trap behavior
remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6178f71f-7538-4802-80b1-7ff2e0cc98f2

📥 Commits

Reviewing files that changed from the base of the PR and between 5c269c1 and c981e8e.

📒 Files selected for processing (1)
  • install.sh

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
install.sh (1)

405-426: ⚠️ Potential issue | 🟡 Minor

nvm_tmp is never removed from the _cleanup_files tracking array.

Line 405 adds nvm_tmp to _cleanup_files, but the manual deletions on lines 408, 421, and 426 never call remove_tracked_file. The EXIT trap's cleanup() function will keep a stale pathname and can later rm -rf a reused path. The spin() function (lines 250–285) demonstrates the correct pattern: after manual rm -f, it immediately calls remove_tracked_file to untrack the entry.

Add remove_tracked_file "$nvm_tmp" after each of the three rm -f "$nvm_tmp" calls.

Proposed fix
   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"
     }
@@
   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
@@
   spin "Installing nvm..." bash "$nvm_tmp"
   rm -f "$nvm_tmp"
+  remove_tracked_file "$nvm_tmp"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` around lines 405 - 426, The code adds nvm_tmp to the
_cleanup_files tracking array but never untracks it when the file is manually
removed, leaving cleanup() to later try to remove a reused path; after every
manual rm -f "$nvm_tmp" in this block (the curl failure branch, the
integrity-failure branch, and after spin finishes) call remove_tracked_file
"$nvm_tmp" to remove it from the tracker (follow the pattern used in spin()).
Update references around nvm_tmp, spin(), and cleanup() accordingly so the EXIT
trap won't operate on a stale path.
♻️ Duplicate comments (1)
install.sh (1)

137-140: ⚠️ Potential issue | 🟠 Major

Move trap registration out of top-level scope.

Lines 137-140 run even when install.sh is sourced, while line 728 explicitly skips main() in that mode. This means anyone sourcing this file will inherit these EXIT/INT/TERM/HUP handlers in their shell. A subsequent Ctrl+C could then terminate the caller's shell instead of just interrupting a command.

🐛 Proposed fix
+install_cleanup_traps() {
+  trap cleanup EXIT
+  trap 'trap_signal INT' INT
+  trap 'trap_signal TERM' TERM
+  trap 'trap_signal HUP' HUP
+}
+
-trap cleanup EXIT
-trap 'trap_signal INT' INT
-trap 'trap_signal TERM' TERM
-trap 'trap_signal HUP' HUP
-
 main() {
+  install_cleanup_traps
   # Parse flags
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` around lines 137 - 140, The trap registrations (trap cleanup EXIT
and trap 'trap_signal ...') are executed at top-level even when the script is
sourced; move those trap setup calls into the execution-only path so they run
only when the script is executed (not sourced). Concretely, register traps
(using cleanup and trap_signal) inside the main execution guard or at the start
of main() and ensure the script uses the standard sourcing check (compare
BASH_SOURCE[0] to $0) so sourcing the file does not install EXIT/INT/TERM/HUP
handlers into the caller's shell.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@install.sh`:
- Around line 116-119: The cleanup loop currently kills wrapper PIDs stored in
_cleanup_pids but not their descendant process groups; update the logic in the
loop that iterates over _cleanup_pids (used by spin()) to send the signal to the
process group instead of the single PID (use kill -TERM -- -$pid or equivalent)
so that child processes started by bash -c (npm/git/curl) are terminated; keep
the existing stderr redirection and tolerated-failure behavior (2>/dev/null ||
true) and apply this change inside the same loop that references _cleanup_pids
and spin().
- Around line 528-530: pre_extract_openclaw() currently creates tmpdir with
mktemp -d in a child shell but has no child-local trap, so interrupts during npm
pack or tar can leave the temp dir behind; modify the child block (where tmpdir
is created and npm pack / tar run) to register a trap that removes the tmpdir on
EXIT/INT/TERM (e.g. trap 'rm -rf "$tmpdir"' EXIT INT TERM) immediately after
creating tmpdir and before running npm pack/tar so cleanup always runs in the
child, and ensure the trap uses the same tmpdir variable name used in
pre_extract_openclaw.

---

Outside diff comments:
In `@install.sh`:
- Around line 405-426: The code adds nvm_tmp to the _cleanup_files tracking
array but never untracks it when the file is manually removed, leaving cleanup()
to later try to remove a reused path; after every manual rm -f "$nvm_tmp" in
this block (the curl failure branch, the integrity-failure branch, and after
spin finishes) call remove_tracked_file "$nvm_tmp" to remove it from the tracker
(follow the pattern used in spin()). Update references around nvm_tmp, spin(),
and cleanup() accordingly so the EXIT trap won't operate on a stale path.

---

Duplicate comments:
In `@install.sh`:
- Around line 137-140: The trap registrations (trap cleanup EXIT and trap
'trap_signal ...') are executed at top-level even when the script is sourced;
move those trap setup calls into the execution-only path so they run only when
the script is executed (not sourced). Concretely, register traps (using cleanup
and trap_signal) inside the main execution guard or at the start of main() and
ensure the script uses the standard sourcing check (compare BASH_SOURCE[0] to
$0) so sourcing the file does not install EXIT/INT/TERM/HUP handlers into the
caller's shell.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 31be302c-0d9c-4df2-beaa-c4e784c6971c

📥 Commits

Reviewing files that changed from the base of the PR and between c981e8e and 4720693.

📒 Files selected for processing (1)
  • install.sh

@stevenobiajulu
Copy link
Copy Markdown
Author

Pushed a follow-up commit that:

  • scopes cleanup trap registration to the execution path, so sourcing install.sh does not install caller-shell traps
  • untracks nvm_tmp after explicit rm -f paths in install_nodejs()
  • adds child-local tmpdir cleanup in pre_extract_openclaw()
  • adds a test covering the sourced-script trap behavior

I did not apply the process-group kill suggestion. spin() does not create a dedicated process group, so kill -- -$pid would be unsafe in non-interactive contexts.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
install.sh (1)

69-143: Cleanup infrastructure is well-designed.

The signal handling follows best practices: trap_signal() runs cleanup, removes the trap, then re-raises the signal so the parent process sees the correct exit status. The guards at lines 79, 98, 116, and 121 correctly prevent Bash 3.2 + set -u unbound variable errors on empty arrays.

One minor hygiene suggestion: declare loop variables as local in cleanup() to avoid polluting the global namespace.

♻️ Optional: declare loop variables local
 cleanup() {
   local rc=$?
   set +e
+  local pid f
   if (( ${`#_cleanup_pids`[@]} )); then
     for pid in "${_cleanup_pids[@]}"; do
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@install.sh` around lines 69 - 143, The cleanup() function uses loop variables
that are currently global (for pid and f), so declare them as local to avoid
polluting the global namespace; update cleanup() to add local declarations like
local pid f (or separate local pid and local f) before their respective
for-loops so the variables are scoped to cleanup(), leaving
register_cleanup_traps(), trap_signal(), remove_tracked_pid(), and
remove_tracked_file() unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@install.sh`:
- Around line 69-143: The cleanup() function uses loop variables that are
currently global (for pid and f), so declare them as local to avoid polluting
the global namespace; update cleanup() to add local declarations like local pid
f (or separate local pid and local f) before their respective for-loops so the
variables are scoped to cleanup(), leaving register_cleanup_traps(),
trap_signal(), remove_tracked_pid(), and remove_tracked_file() unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0086a94d-615e-4a42-b8d0-2e0d0a1d88b1

📥 Commits

Reviewing files that changed from the base of the PR and between 7398f15 and 73053bf.

📒 Files selected for processing (2)
  • install.sh
  • test/install-preflight.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/install-preflight.test.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[NemoClaw] install.sh spin() has no trap handler — Ctrl+C orphans background processes and leaks temp files

1 participant