Finishes the pass: every script now documents its safeguards, and the deliberate absences in the sourced libraries are recorded so they are not "corrected" later.
290 lines
11 KiB
Bash
Executable File
290 lines
11 KiB
Bash
Executable File
#!/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.
|
|
#
|
|
# ==============================================================================================
|
|
# DESIGN PRINCIPLES
|
|
# ==============================================================================================
|
|
#
|
|
# Name the Scripts, Not Just the PIDs
|
|
# Output resolves each process to the script it is running. "Stopping 4 processes" tells
|
|
# an operator nothing they can act on; "stopping mover_tuning, preclear" tells them
|
|
# exactly what they are about to lose and whether they should wait.
|
|
#
|
|
# Targeted, Never Blanket
|
|
# Only processes spawned by the User Scripts plugin are matched. A broad pattern would
|
|
# catch unrelated shells and Varaverk's own scripts — including, during a reboot sequence,
|
|
# the very script doing the stopping.
|
|
#
|
|
# Shutdown Step and Manual Tool, Same Behaviour
|
|
# Called automatically by server_reboot.sh and usable by hand on a stuck script. It takes
|
|
# no "reboot mode" — the correct action is identical either way, and a mode flag would be
|
|
# a second code path that only ever runs unattended.
|
|
#
|
|
# ==============================================================================================
|
|
# 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.
|
|
#
|
|
# ==============================================================================================
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
#
|
|
# No config vars. Targets are discovered from running processes rather than from a list,
|
|
# because the User Scripts plugin owns what exists and any list here would go stale.
|
|
#
|
|
# Platform-owned:
|
|
#
|
|
# The unRAID User Scripts plugin's script directory and the processes it spawns. Matching
|
|
# is scoped to those — see Targeted, Never Blanket above for why that matters during a
|
|
# reboot sequence.
|
|
#
|
|
# Called by server_reboot.sh as a shutdown step; also safe to run by hand.
|
|
#
|
|
# ==============================================================================================
|
|
# 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 |