#!/bin/bash # ============================================================================================== # ============================= User Scripts Stop ============================================== # ============================================================================================== # Stops all running User Script processes spawned by the unRAID User Scripts plugin. # Identifies processes by their /tmp/user.scripts path signature. # Shows script names not just PIDs — you know what's being stopped. # # ── WHEN TO USE ─────────────────────────────────────────────────────────────────────────────── # - Before a planned reboot when scripts are running mid-cycle # - When a script is stuck and won't respond to the Abort button in the UI # - Called automatically by server_reboot.sh as part of shutdown sequence # - Emergency stop of all background ecosystem scripts # # ── HOW IT IDENTIFIES PROCESSES ─────────────────────────────────────────────────────────────── # Scans /proc/*/cmdline for processes whose command line contains "/tmp/user.scripts". # The unRAID User Scripts plugin stages all scripts in /tmp/user.scripts/ before execution. # This is more reliable than process name matching which can vary. # # ── STOP SEQUENCE PER PROCESS ───────────────────────────────────────────────────────────────── # 1. Send SIGTERM — allows script to trap and clean up gracefully # 2. Wait 5 seconds # 3. Check if still running → SIGKILL (force) if SIGTERM ignored # 4. Verify dead after SIGKILL # # ── SELF-EXCLUSION ──────────────────────────────────────────────────────────────────────────── # If this script itself is run via the User Scripts plugin it would find its own PID. # Self-exclusion prevents this script from killing itself mid-execution. # # ── SAFEGUARDS ──────────────────────────────────────────────────────────────────────────────── # Root check — kill requires root for other users' processes # acquire_lock — prevents concurrent stop attempts # Self-exclusion — never kills its own process tree # SIGTERM → SIGKILL — graceful then forced # Verify after kill — confirms processes are actually dead # validate_unraid_cmd — notify validated before use # Silent when clean — no processes running = log() only ✅ # # ── USAGE ───────────────────────────────────────────────────────────────────────────────────── # user_scripts_stop.sh — stop all user scripts # user_scripts_stop.sh --dry-run — show what would be stopped # user_scripts_stop.sh --status — show currently running user scripts # user_scripts_stop.sh --log — verbose output # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" parse_args "$@" MY_PID=$$ MY_PPID=$PPID # ============================================================================================== # ━━━ Setup ━━━ # ============================================================================================== if [[ "$EUID" -ne 0 ]]; then error "Must be run as root — kill requires root for other users' processes" exit 1 fi validate_unraid_cmd \ "/usr/local/emhttp/plugins/dynamix/scripts/notify" \ "" "" \ "unRAID notify script" || warn "unRAID notify script not found — native notifications disabled" acquire_lock detect_hosts [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no processes will be killed" # ============================================================================================== # ── HELPER FUNCTIONS ────────────────────────────────────────────────────────────────────────── # ============================================================================================== # Get script name from PID — extracts meaningful name from /tmp/user.scripts path get_script_name() { local pid="$1" local cmdline cmdline=$(tr '\0' ' ' < /proc/"$pid"/cmdline 2>/dev/null || echo "") # Extract the script filename from the /tmp/user.scripts/... path echo "$cmdline" | grep -o '/tmp/user\.scripts[^ ]*' | \ awk -F/ '{print $NF}' | head -1 || echo "pid-$pid" } # Get all user script PIDs — excludes self and own parent process tree get_user_script_pids() { local -a pids=() while IFS= read -r pid; do [[ -z "$pid" ]] && continue # Self-exclusion — don't kill our own process or parent [[ "$pid" == "$MY_PID" ]] && continue [[ "$pid" == "$MY_PPID" ]] && continue pids+=("$pid") done < <( for dir in /proc/[0-9]*/cmdline; do pid="${dir%/cmdline}" pid="${pid#/proc/}" if grep -ql '/tmp/user\.scripts' "$dir" 2>/dev/null; then echo "$pid" fi done ) printf '%s\n' "${pids[@]}" } # ============================================================================================== # ━━━ Status ━━━ # ============================================================================================== if [[ "$SHOW_STATUS" == true ]]; then echo "" echo "━━━━━ $ICON_SUMMARY USER SCRIPTS STATUS ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "" mapfile -t PIDS < <(get_user_script_pids) if [[ ${#PIDS[@]} -eq 0 ]]; then log "No User Script processes running" else echo " ${#PIDS[@]} User Script process(es) running:" for pid in "${PIDS[@]}"; do name=$(get_script_name "$pid") elapsed=$(ps -o etimes= -p "$pid" 2>/dev/null | tr -d ' ') runtime=$(format_duration "${elapsed:-0}") echo " $ICON_RUNNING PID $pid — $name (${runtime})" done fi echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi # ============================================================================================== # ━━━ User Scripts Stop ━━━ # ============================================================================================== echo "" echo "━━━ $ICON_PLUGIN User Scripts Stop — $MY_ID ━━━" START=$(date +%s) mapfile -t PIDS < <(get_user_script_pids) KILLED=() FAILED=() SKIPPED=() if [[ ${#PIDS[@]} -eq 0 ]]; then log "No User Script processes running — nothing to do" else warn "${#PIDS[@]} User Script process(es) found" echo "" for pid in "${PIDS[@]}"; do name=$(get_script_name "$pid") if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would stop: $name (PID $pid)" SKIPPED+=("$name") continue fi # Verify still running before trying to kill if ! kill -0 "$pid" 2>/dev/null; then log "$name (PID $pid) — already exited" continue fi # SIGTERM — graceful stop log "Sending SIGTERM to $name (PID $pid)..." kill -TERM "$pid" 2>/dev/null || true sleep 5 # Check if stopped after SIGTERM if ! kill -0 "$pid" 2>/dev/null; then warn "Stopped: $name (PID $pid) ✅" KILLED+=("$name") continue fi # SIGKILL — forced stop warn "$name still running after SIGTERM — sending SIGKILL" kill -KILL "$pid" 2>/dev/null || true sleep 2 # Final verify if ! kill -0 "$pid" 2>/dev/null; then warn "Force-stopped: $name (PID $pid) ✅" KILLED+=("$name") else error "Failed to kill: $name (PID $pid)" FAILED+=("$name") fi done fi END=$(date +%s) # ============================================================================================== # ━━━ Summary ━━━ # ============================================================================================== echo "" echo "━━━━━ $ICON_SUMMARY USER SCRIPTS STOP SUMMARY ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_TIME Duration: $(format_duration $(( END - START )))" echo "" if [[ ${#PIDS[@]} -eq 0 ]]; then log "No processes were running" elif [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would have stopped ${#SKIPPED[@]} process(es): ${SKIPPED[*]}" else [[ ${#KILLED[@]} -gt 0 ]] && warn "Stopped (${#KILLED[@]}): ${KILLED[*]}" [[ ${#FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Failed (${#FAILED[@]}): ${FAILED[*]}" fi echo "" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — no changes made" elif [[ ${#FAILED[@]} -gt 0 ]]; then echo "$ICON_ERROR Status: SOME PROCESSES COULD NOT BE KILLED" notify "User Scripts stop failed on $(hostname) ($MY_ID) — unkillable: ${FAILED[*]}" \ "User Scripts Stop" "warning" else log "$ICON_DONE Status: done ✅" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" [[ ${#FAILED[@]} -gt 0 ]] && exit 1 exit 0