Auth stack certs tab, arrs db fallbacks, cert monitor cache, conf parser fix

- Auth stack: fold cert monitor into Auth Stack page as fourth tab (Certs);
  remove standalone cert page and top-level tab
- cert_monitor.sh: write JSON status cache to State_Files/cert_status.json
  after each run; expose per-domain days/expiry via _CERT_DAYS/_CERT_EXPIRY globals
- api/cert.php: new — serves cached cert status; falls back to configured
  domains as UNKN when no cache exists; POST action=run triggers live check
- arrs db fallbacks: vv_arr_cleanup_stats/discovery_stats/recovery_stats now
  read from data/*.db files when log JSON files don't yet exist
- config.php vv_conf_vars(): unescape bash \$ → $ so passwords with dollar
  signs read correctly from conf files
- host1.conf: fill in HOST1_NPM_USER/PASS and HOST1_LLDAP_USER/PASS
- Partnership adapter pattern: Unraid-specific container logic extracted to
  Plugin/unraid/Partnership/; platform-agnostic structure stays in Partnership/
- First-run wizard: uniform multi-step flow for all hosts; HOST2 pull moved
  to checklist; auto SSH keygen and API key creation on save
- api/checklist.php: live setup checklist with pull_master action
- Fullscreen toggle: hide Unraid header/menu; state persists via localStorage
This commit is contained in:
Gmer4Lfe
2026-06-05 23:17:30 -04:00
parent 17012bcd8c
commit 9c3ace95a7
43 changed files with 1605 additions and 1167 deletions
+195
View File
@@ -0,0 +1,195 @@
#!/bin/bash
# ==============================================================================================
# ================================= Mover Stop =================================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Safely stops the unRAID mover with a wall warning, configurable timeout, and
# SIGTERM → SIGKILL sequence. Use before planned reboots, disk operations, or
# any operation where mover and rsync running simultaneously could corrupt files.
# Exits cleanly if mover is not running.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Stop Sequence
# 1. Check if mover is running — exit cleanly if not
# 2. Wall message to all logged-in terminal users
# 3. Wait MOVER_STOP_TIMEOUT seconds
# 4. SIGTERM — allows mover to finish its current file before stopping
# (no partial files — the mover completes what it is working on)
# 5. Wait 5 seconds → verify stopped
# 6. SIGKILL if still running — forced stop, partial files possible
# 7. Final verify — error if still running after SIGKILL
#
# SIGTERM first because the mover has an opportunity to finish the file it is
# currently moving, leaving no partial copies on cache or array. SIGKILL is only
# used as a last resort and may leave a file split across cache and array.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Single Instance Lock
# acquire_lock prevents concurrent stop attempts racing each other.
#
# Root Required
# pkill on emhttp processes requires root.
#
# Final Verify
# Confirms mover is actually stopped after the kill sequence — errors if it
# is still running after SIGKILL.
#
# Silent When Clean
# Mover not running = log() only, no visible output.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
# MOVER_STOP_TIMEOUT
# Seconds between wall warning and SIGTERM. (default: 30)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# mover_stop.sh
# Check if mover is running. If so, warn users and stop it.
#
# mover_stop.sh --dry-run
# Show mover state and what would happen. No signals sent.
#
# mover_stop.sh --status
# Show mover state (running, PID, start time). Then exit.
#
# mover_stop.sh --log
# Verbose output showing each step of the stop sequence.
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../load_config.sh"
parse_args "$@"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root — pkill on emhttp processes requires root"
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"
validate_int MOVER_STOP_TIMEOUT "$MOVER_STOP_TIMEOUT"
acquire_lock
detect_hosts
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_MOVER Timeout: ${MOVER_STOP_TIMEOUT}s"
echo "$ICON_GEAR Dry Run: $DRY_RUN"
echo ""
if platform_is_mover_running; then
MOVER_PID=$(platform_get_mover_pid)
MOVER_START=$(ps -o lstart= -p "$MOVER_PID" 2>/dev/null | xargs)
echo " $ICON_MOVER Mover: RUNNING (PID $MOVER_PID)"
[[ -n "$MOVER_START" ]] && echo " $ICON_TIME Started: $MOVER_START"
else
echo " $ICON_MOVER Mover: not running"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
# ==============================================================================================
# ━━━ Mover Stop ━━━
# ==============================================================================================
START=$(date +%s)
if ! platform_is_mover_running; then
echo "Mover is not running — nothing to do"
exit 0
fi
MOVER_PID=$(platform_get_mover_pid)
MOVER_START=$(ps -o lstart= -p "$MOVER_PID" 2>/dev/null | xargs)
MOVER_ELAPSED=$(ps -o etimes= -p "$MOVER_PID" 2>/dev/null | tr -d ' ')
warn "Mover is running (PID $MOVER_PID) — stopping in ${MOVER_STOP_TIMEOUT}s"
log "$ICON_TIME Mover started: ${MOVER_START:-unknown} — running for $(format_duration "${MOVER_ELAPSED:-0}")"
# ── Warn users via wall ───────────────────────────────────────────────────────────────────────
if [[ "$DRY_RUN" == false ]]; then
wall "$ICON_WARN $MY_ID ($LOCAL_SERVER_NAME) — unRAID Mover stopping in ${MOVER_STOP_TIMEOUT}s"
echo "Wall message sent — waiting ${MOVER_STOP_TIMEOUT}s..."
sleep "$MOVER_STOP_TIMEOUT"
else
warn "DRY RUN — would send wall warning and wait ${MOVER_STOP_TIMEOUT}s"
fi
# ── SIGTERM — graceful stop ───────────────────────────────────────────────────────────────────
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would send SIGTERM to mover (PID $MOVER_PID)"
else
log "Sending SIGTERM to mover (PID $MOVER_PID)..."
kill -TERM "$MOVER_PID" 2>/dev/null || true
sleep 5
# Verify stopped after SIGTERM
if ! platform_is_mover_running; then
warn "Mover stopped cleanly (SIGTERM) ✅"
else
# ── SIGKILL — forced stop ─────────────────────────────────────────────────────────────
warn "Mover still running after SIGTERM — sending SIGKILL (may leave partial files)"
kill -KILL "$MOVER_PID" 2>/dev/null || true
sleep 2
# Final verify
if platform_is_mover_running; then
error "Mover still running after SIGKILL — manual intervention needed"
notify "Mover stop failed on $(hostname) ($MY_ID) — process unkillable" \
"Mover Stop" "warning"
exit 1
else
warn "Mover force-stopped (SIGKILL) — check for partial files on cache"
fi
fi
fi
END=$(date +%s)
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY MOVER STOP SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_MOVER Timeout: ${MOVER_STOP_TIMEOUT}s"
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
echo ""
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — no changes made"
else
echo "$ICON_DONE Status: done — mover stopped ✅"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+247
View File
@@ -0,0 +1,247 @@
#!/bin/bash
# ==============================================================================================
# ============================= PHP-FPM Max Children ===========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Raises PHP-FPM pm.max_children to prevent WebGUI slowdowns under load. Run
# once at array start via ARRAY_START_SCRIPTS. Idempotent — silent when the
# value is already correct, no restart on clean boot.
#
# unRAID's WebGUI runs through PHP-FPM. The default pm.max_children is very
# low (48). Under load — multiple users, Docker operations, heavy dashboard
# usage — all PHP workers saturate and new requests queue. The WebGUI becomes
# slow or unresponsive.
#
# PHP_MAX_CHILDREN=250 is appropriate for 128GB RAM: ~2MB per worker = ~500MB
# total. Too high wastes RAM; too low causes slowdowns.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Idempotent Content Check
# Reads the current pm.max_children value before writing. If already at
# target → silent exit, no PHP-FPM restart. Restarting PHP-FPM unnecessarily
# disrupts active WebGUI sessions on every boot.
#
# Pattern Match Before Write
# Verifies the sed pattern finds pm.max_children in the config before
# applying any change. Prevents silent failures where sed succeeds but
# writes nothing because the key was missing or commented out.
#
# Apply Sequence
# 1. Read current pm.max_children from PHP_CONF
# 2. If already at target → exit silently
# 3. Verify sed pattern matches before writing
# 4. Apply sed replacement
# 5. Restart PHP-FPM via rc.php-fpm
# 6. Verify PHP-FPM process running after restart
# 7. Read back config to confirm value applied
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Required
# Writing to /etc/php83/ requires root.
#
# Single Instance Lock
# acquire_lock prevents concurrent runs at array start.
#
# Process Verify
# Confirms PHP-FPM running after restart — errors if it failed to start.
#
# Config Verify
# Reads back config after restart to confirm the value was actually applied.
#
# Silent on Success
# Runs every boot — no noise when already correct.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
# PHP_MAX_CHILDREN
# Target pm.max_children value. (default: 250)
#
# PHP_CONF
# Path to PHP-FPM www.conf. (default: /etc/php83/php-fpm.d/www.conf)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# php_fpm_max_children.sh
# Read current value. Update and restart PHP-FPM only if changed. Silent if correct.
#
# php_fpm_max_children.sh --dry-run
# Show current vs target value. No config write or restart.
#
# php_fpm_max_children.sh --status
# Show current pm.max_children, target, and PHP-FPM process state.
#
# php_fpm_max_children.sh --log
# Verbose output showing idempotent check, config write, and restart result.
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../load_config.sh"
parse_args "$@"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root — writing system config requires root"
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"
validate_int PHP_MAX_CHILDREN "$PHP_MAX_CHILDREN"
require_var PHP_CONF
acquire_lock
detect_hosts
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY PHP-FPM STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_GEAR Config file: $PHP_CONF"
echo "$ICON_PHP Target: pm.max_children = $PHP_MAX_CHILDREN"
echo ""
if [[ -f "$PHP_CONF" ]]; then
CURRENT_VAL=$(grep -E "^pm\.max_children" "$PHP_CONF" 2>/dev/null | \
awk '{print $NF}')
if [[ "${CURRENT_VAL:-0}" -eq "$PHP_MAX_CHILDREN" ]]; then
echo " $ICON_SUCCESS Current: pm.max_children = $CURRENT_VAL (correct ✅)"
else
echo " $ICON_WARN Current: pm.max_children = ${CURRENT_VAL:-not set} (would update)"
fi
else
echo " $ICON_ERROR Config file not found: $PHP_CONF"
fi
echo ""
if pgrep -f "php-fpm" >/dev/null 2>&1; then
FPM_COUNT=$(pgrep -fc "php-fpm" 2>/dev/null || echo "?")
echo " $ICON_SUCCESS PHP-FPM: running ($FPM_COUNT worker(s))"
else
echo " $ICON_ERROR PHP-FPM: NOT running"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
# ==============================================================================================
# ━━━ PHP-FPM Config ━━━
# ==============================================================================================
START=$(date +%s)
if [[ ! -f "$PHP_CONF" ]]; then
error "PHP config file not found: $PHP_CONF"
notify "PHP-FPM config not found on $(hostname) ($MY_ID) — $PHP_CONF missing" \
"PHP-FPM" "warning"
exit 1
fi
# ── Idempotent check ─────────────────────────────────────────────────────────────────────────
CURRENT_VAL=$(grep -E "^pm\.max_children" "$PHP_CONF" 2>/dev/null | awk '{print $NF}')
if [[ "${CURRENT_VAL:-0}" -eq "$PHP_MAX_CHILDREN" ]]; then
echo "pm.max_children already $PHP_MAX_CHILDREN"
exit 0
fi
warn "pm.max_children: ${CURRENT_VAL:-not set}$PHP_MAX_CHILDREN"
# ── Verify pattern exists before writing ─────────────────────────────────────────────────────
if ! grep -qE "^pm\.max_children" "$PHP_CONF" 2>/dev/null; then
error "pm.max_children not found in $PHP_CONF — cannot apply"
error "Add 'pm.max_children = $PHP_MAX_CHILDREN' to $PHP_CONF manually"
notify "PHP-FPM pm.max_children not found in config on $(hostname) ($MY_ID)" \
"PHP-FPM" "warning"
exit 1
fi
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would set pm.max_children = $PHP_MAX_CHILDREN in $PHP_CONF"
warn "DRY RUN — would restart PHP-FPM"
exit 0
fi
# ── Apply setting ─────────────────────────────────────────────────────────────────────────────
log "Applying pm.max_children = $PHP_MAX_CHILDREN..."
if ! sed -i "s/^pm\.max_children.*/pm.max_children = $PHP_MAX_CHILDREN/" "$PHP_CONF"; then
error "Failed to update $PHP_CONF"
notify "PHP-FPM config update failed on $(hostname) ($MY_ID)" \
"PHP-FPM" "warning"
exit 1
fi
log "Config updated"
# ── Restart PHP-FPM ──────────────────────────────────────────────────────────────────────────
log "Restarting PHP-FPM..."
if ! platform_restart_service php-fpm; then
error "PHP-FPM restart command failed"
notify "PHP-FPM restart failed on $(hostname) ($MY_ID)" \
"PHP-FPM" "warning"
exit 1
fi
sleep 3 # Allow PHP-FPM workers to initialise
# ── Verify process running ────────────────────────────────────────────────────────────────────
if ! pgrep -f "php-fpm" >/dev/null 2>&1; then
error "PHP-FPM not running after restart — WebGUI may be broken"
notify "PHP-FPM failed to start after config update on $(hostname) ($MY_ID)" \
"PHP-FPM" "warning"
exit 1
fi
# ── Verify config reflects target ────────────────────────────────────────────────────────────
APPLIED_VAL=$(grep -E "^pm\.max_children" "$PHP_CONF" 2>/dev/null | awk '{print $NF}')
if [[ "${APPLIED_VAL:-0}" -ne "$PHP_MAX_CHILDREN" ]]; then
warn "Config reads pm.max_children = ${APPLIED_VAL:-unknown} — expected $PHP_MAX_CHILDREN"
warn "Check $PHP_CONF manually"
else
log "Verified: pm.max_children = $APPLIED_VAL"
fi
FPM_WORKERS=$(pgrep -fc "php-fpm" 2>/dev/null || echo "?")
log "$ICON_PHP Workers running: $FPM_WORKERS"
END=$(date +%s)
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY PHP-FPM SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_GEAR Config file: $PHP_CONF"
echo "$ICON_PHP Applied: pm.max_children = $PHP_MAX_CHILDREN"
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
echo ""
echo "$ICON_DONE Status: done ✅"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
+153
View File
@@ -0,0 +1,153 @@
#!/bin/bash
# ==============================================================================================
# ================================= Unraid API Key Renewal ====================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Creates/overwrites the Varaverk API key in the unraid-api service registry at
# array start. The registry is ephemeral — OS updates and service restarts clear
# it. This script re-registers the key every boot so Varaverk's enhanced
# monitoring self-heals without manual intervention.
#
# Also updates HOST*_UNRAID_API_KEY in the local host conf so the partnership
# page always reflects the live key value.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# unraid_api_key_renew.sh
# Renew the key. Silent on success.
#
# unraid_api_key_renew.sh --dry-run
# Show what would happen — no changes made.
#
# unraid_api_key_renew.sh --log
# Verbose output.
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../load_config.sh"
parse_args "$@"
acquire_lock
detect_hosts
# ──────────────────────────────────────────────────────────────────────────────
CONF_FILE="$SCRIPT_DIR/../Configurations/${MY_ID,,}.conf"
VAR_NAME="${MY_ID}_UNRAID_API_KEY"
# Key name: "Varaverk <hostname>" stripping any unraid- prefix
# Space separator — unRAID API only allows letters, numbers, and spaces
HOSTNAME_SUFFIX=$(hostname -s 2>/dev/null | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//' || hostname -s)
KEY_NAME="Varaverk ${HOSTNAME_SUFFIX}"
log "$ICON_GEAR Conf file: $CONF_FILE"
log "$ICON_GEAR Key var: $VAR_NAME"
log "$ICON_GEAR Key name: $KEY_NAME"
if [[ ! -f "$CONF_FILE" ]]; then
error "Conf file not found: $CONF_FILE"
exit 1
fi
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would check registry for $KEY_NAME, renew only if missing"
exit 0
fi
# ──────────────────────────────────────────────────────────────────────────────
# Check if key already exists in the unraid-api registry before creating.
# --overwrite generates a new key value every time, invalidating the old one.
# Only renew if the registry has lost it.
log "Checking unraid-api registry for $KEY_NAME..."
EXISTING=$(timeout 5 /usr/local/sbin/unraid-api apikey --name "$KEY_NAME" --json </dev/null 2>/dev/null)
KEY=$(echo "$EXISTING" | jq -r '.key // empty' 2>/dev/null)
if [[ -n "$KEY" ]]; then
PREVIEW="${KEY:0:8}...${KEY: -4}"
echo "API key valid ✅ — $VAR_NAME = $PREVIEW"
log "Key found in registry — no renewal needed"
exit 0
fi
log "Key not found in registry — creating new key..."
RAW=$(timeout 10 /usr/local/sbin/unraid-api apikey \
--name "$KEY_NAME" --create --overwrite \
--description "Varaverk plugin" --roles ADMIN --json </dev/null 2>&1)
if [[ -z "$RAW" ]]; then
error "unraid-api returned no output"
exit 1
fi
KEY=$(echo "$RAW" | jq -r '.key // empty' 2>/dev/null)
if [[ -z "$KEY" ]]; then
error "No key in unraid-api response: ${RAW:0:200}"
exit 1
fi
# ──────────────────────────────────────────────────────────────────────────────
if grep -q "^\s*${VAR_NAME}\s*=" "$CONF_FILE"; then
sed -i "s|^\(\s*${VAR_NAME}\s*=\s*\)\"[^\"]*\"|\1\"${KEY}\"|" "$CONF_FILE"
else
echo " ${VAR_NAME}=\"${KEY}\"" >> "$CONF_FILE"
fi
PREVIEW="${KEY:0:8}...${KEY: -4}"
log "Writing new key to: $CONF_FILE"
warn "API key renewed ✅ — $VAR_NAME = $PREVIEW (registry had lost it)"
# ── Push renewed key into each partner's OWN conf ─────────────────────────────
# Each host's conf is its complete keychest — no cross-host conf files needed.
# SSH_KEY is set by detect_hosts() — this server's outbound private key.
if [[ -z "$SSH_KEY" ]]; then
log "No SSH key configured — skipping partner push"
exit 0
fi
for host_var in $(compgen -v | grep -E '^HOST[0-9]+$'); do
partner_host="${!host_var}"
[[ -z "$partner_host" ]] && continue
[[ "${host_var,,}" == "${MY_ID,,}" ]] && continue
partner_slot="${host_var,,}" # e.g. host2
partner_ip=$(resolve_tailscale_ip "$partner_host" 2>/dev/null || true)
[[ -z "$partner_ip" ]] && { log "Cannot resolve IP for $partner_host — skipping"; continue; }
# Target is the partner's OWN conf on their machine
partner_conf="/boot/config/plugins/varaverk/Configurations/${partner_slot}.conf"
tmp=$(mktemp /tmp/vv_kp_XXXXXX.sh)
remote="/tmp/vv_kp_${RANDOM}.sh"
chmod 700 "$tmp"
# Key stays in the temp file — never appears in SSH command args
cat > "$tmp" <<PUSHSCRIPT
#!/bin/sh
target='${partner_conf}'
if grep -q "\b${VAR_NAME}\b" "\$target" 2>/dev/null; then
sed -i 's|^\(\\s*${VAR_NAME}\\s*=\\s*\)"[^"]*"|\1"${KEY}"|' "\$target"
else
printf ' ${VAR_NAME}="%s"\n' '${KEY}' >> "\$target"
fi
echo ok
PUSHSCRIPT
if timeout 10 scp -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \
-o StrictHostKeyChecking=no "$tmp" "root@${partner_ip}:${remote}" 2>/dev/null; then
if timeout 10 ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \
-o StrictHostKeyChecking=no "root@${partner_ip}" \
"bash '${remote}'; rc=\$?; rm -f '${remote}'; exit \$rc" 2>/dev/null | grep -q ok; then
log "Key pushed to $partner_host"
else
warn "Key push to $partner_host failed — they can create their own copy"
fi
else
warn "SCP to $partner_host failed — skipping"
fi
rm -f "$tmp"
done
+256
View File
@@ -0,0 +1,256 @@
#!/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