#!/bin/bash # ============================================================================================== # ============================= User Scripts Stop ============================================== # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Stops all running User Script processes spawned by the unRAID User Scripts # plugin. Shows script names not just PIDs so you know what's being stopped. # Called automatically by server_reboot.sh as part of the shutdown sequence, # and useful directly when a script is stuck and won't respond to the UI. # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # Process Identification # Scans /proc/*/cmdline for processes whose command line contains # "/tmp/user.scripts". The User Scripts plugin stages all scripts in # /tmp/user.scripts/ before execution — more reliable than process name # matching which can vary. # # Stop Sequence Per Process # 1. Send SIGTERM — allows the script to trap and clean up gracefully # 2. Wait 5 seconds # 3. If still running → SIGKILL (force) # 4. Verify dead after SIGKILL — error if still running # # Self-Exclusion # If this script is run via the User Scripts plugin it would find its own # PID in the scan. Self-exclusion by PID prevents killing its own process # tree mid-execution. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Root Required # kill requires root for other users' processes. # # Single Instance Lock # acquire_lock prevents concurrent stop attempts. # # SIGTERM → SIGKILL Sequence # Graceful first. Forced only if SIGTERM ignored after 5 seconds. # # Post-Kill Verify # Confirms each process is actually dead. Errors and notifies if unkillable. # # Silent When Clean # No processes running = log() only, no visible output. # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # user_scripts_stop.sh # Find and stop all User Script processes. Silent if none running. # # user_scripts_stop.sh --dry-run # Show which processes would be stopped, with names and runtimes. No kills. # # user_scripts_stop.sh --status # Show currently running User Script processes with names and elapsed time. # # user_scripts_stop.sh --log # Verbose output — show each process found, each signal sent, each result. # # ============================================================================================== 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 platform_require_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 ) (( ${#pids[@]} > 0 )) && 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 echo "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 echo "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 echo "$ICON_DONE Status: done ✅" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" [[ ${#FAILED[@]} -gt 0 ]] && exit 1 exit 0