Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 361 additions & 0 deletions scripts/runewager_redeploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,361 @@
#!/usr/bin/env bash
#
# runewager_redeploy.sh — One-Shot Nuclear Redeploy
# Runewager Bot — Hostinger KVM1
#
# What this does (in order):
# 1. Stop the runewager systemd service
# 2. Kill anything still holding port 3000
# 3. npm ci — clean dependency rebuild
# 4. Install / restart rw_cpu_guard as a systemd service
# 5. Start the runewager systemd service
# 6. Health-check the bot (waits up to 30 s)
# 7. Launch a background forensics trace (5 min, non-blocking)
# 8. Print a final status summary
#
# Usage:
# sudo ./scripts/runewager_redeploy.sh
# sudo ./scripts/runewager_redeploy.sh --skip-forensics # skip background trace
# sudo ./scripts/runewager_redeploy.sh --dry-run # print steps, no action
#
# Notes:
# - Tooltips are NOT touched — bot-saved tooltips are preserved.
# - Requires root (for systemd + cgroup + port ops).
#

set -uo pipefail

# ─────────────────────────────── IDENTITY ────────────────────────────────────

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
readonly GUARD_SCRIPT="$SCRIPT_DIR/rw_cpu_guard.sh"

BOT_SERVICE="runewager"
BOT_PORT=3000
HEALTH_URL="http://127.0.0.1:${BOT_PORT}/health"
HEALTH_TIMEOUT=30 # seconds to wait for bot to respond

DRY_RUN=0
SKIP_FORENSICS=0

# ─────────────────────────────── COLOUR OUTPUT ───────────────────────────────

_bold='\033[1m'
_green='\033[0;32m'
_yellow='\033[1;33m'
_red='\033[0;31m'
_cyan='\033[0;36m'
_reset='\033[0m'

ts() { date '+%H:%M:%S'; }
hdr() { echo -e "\n${_bold}${_cyan}╔══ $* ══╗${_reset}"; }
ok() { echo -e " ${_green}✔${_reset} $*"; }
warn() { echo -e " ${_yellow}⚠${_reset} $*"; }
fail() { echo -e " ${_red}✘${_reset} $*"; }
step() { echo -e "\n${_bold}[$(ts)] STEP $*${_reset}"; }

run() {
# run <description> <cmd...>
local desc="$1"; shift
echo -e " ${_cyan}→${_reset} ${desc}"
if (( DRY_RUN )); then
echo -e " ${_yellow}[DRY-RUN]${_reset} $*"
return 0
fi
if "$@"; then
ok "$desc"
else
warn "$desc returned non-zero (continuing)"
fi
}

# ─────────────────────────────── CHECKS ──────────────────────────────────────

_check_root() {
if [[ "$(id -u)" -ne 0 ]]; then
fail "This script must be run as root (sudo $0)"
exit 1
fi
}

_check_app_dir() {
if [[ ! -f "$APP_DIR/index.js" ]]; then
# Try VPS default path
local vps_path="/var/www/html/Runewager"
if [[ -f "$vps_path/index.js" ]]; then
# Redefine app-relative paths
warn "index.js not at $APP_DIR — using VPS path $vps_path"
# shellcheck disable=SC2034
APP_DIR_OVERRIDE="$vps_path"
else
fail "Cannot find index.js — is this the right directory? ($APP_DIR)"
exit 1
fi
fi
ok "App dir: ${APP_DIR_OVERRIDE:-$APP_DIR}"
}

# Use APP_DIR_OVERRIDE if set (VPS path fallback)
_app() { echo "${APP_DIR_OVERRIDE:-$APP_DIR}"; }

# ─────────────────────────────── STEP 1: STOP SERVICE ────────────────────────

_stop_bot_service() {
step "1/8 — Stop ${BOT_SERVICE} systemd service"
if systemctl is-active --quiet "$BOT_SERVICE" 2>/dev/null; then
run "Stop ${BOT_SERVICE}" systemctl stop "$BOT_SERVICE"
else
warn "${BOT_SERVICE} service was not running (OK)"
fi
}

# ─────────────────────────────── STEP 2: KILL PORT ───────────────────────────

_kill_port() {
step "2/8 — Kill anything on port ${BOT_PORT}"
local pids
pids=$(lsof -t -i :"${BOT_PORT}" 2>/dev/null) || true

if [[ -z "$pids" ]]; then
ok "Port ${BOT_PORT} is free"
return 0
fi

echo " Found PIDs on :${BOT_PORT}: ${pids}"
if (( DRY_RUN )); then
warn "[DRY-RUN] would kill PIDs: $pids"
return 0
fi

# SIGTERM first, then SIGKILL stragglers
echo "$pids" | xargs -r kill -TERM 2>/dev/null || true
sleep 2
local remaining
remaining=$(lsof -t -i :"${BOT_PORT}" 2>/dev/null) || true
if [[ -n "$remaining" ]]; then
echo "$remaining" | xargs -r kill -KILL 2>/dev/null || true
ok "Port ${BOT_PORT} force-cleared"
else
ok "Port ${BOT_PORT} cleared cleanly"
fi
}

# ─────────────────────────────── STEP 3: NPM CI ──────────────────────────────

_npm_rebuild() {
step "3/8 — npm ci (clean dependency rebuild)"
local app; app=$(_app)

if [[ ! -f "$app/package.json" ]]; then
warn "No package.json at $app — skipping npm ci"
return 0
fi

if ! command -v node >/dev/null 2>&1; then
fail "node not found — cannot rebuild"
return 1
fi

local node_ver; node_ver=$(node --version 2>/dev/null)
ok "Node version: $node_ver"

if (( DRY_RUN )); then
warn "[DRY-RUN] would run: cd $app && nice -n 19 npm ci --prefer-offline"
return 0
fi

pushd "$app" >/dev/null
# Run at low priority so this install doesn't spike the box
nice -n 19 ionice -c 3 npm ci --prefer-offline 2>&1 \
| tail -5 \
| sed 's/^/ /'
ok "npm ci complete"
popd >/dev/null
Comment on lines +168 to +174

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The npm ci step always logs a successful completion and returns success even if the npm/ionice pipeline fails, which can mislead operators and cause the redeploy to proceed with a broken or incomplete dependency install; checking the pipeline exit code ensures failures are correctly reported and propagated. [logic error]

Severity Level: Major ⚠️
- ⚠️ Redeploy may proceed after failed dependency installation.
- ⚠️ Operators misled by false-success npm ci log message.
Suggested change
pushd "$app" >/dev/null
# Run at low priority so this install doesn't spike the box
nice -n 19 ionice -c 3 npm ci --prefer-offline 2>&1 \
| tail -5 \
| sed 's/^/ /'
ok "npm ci complete"
popd >/dev/null
pushd "$app" >/dev/null
# Run at low priority so this install doesn't spike the box
nice -n 19 ionice -c 3 npm ci --prefer-offline 2>&1 \
| tail -5 \
| sed 's/^/ /'
local npm_status=$?
if (( npm_status == 0 )); then
ok "npm ci complete"
else
warn "npm ci failed with status ${npm_status}"
fi
popd >/dev/null
return "$npm_status"
Steps of Reproduction ✅
1. Introduce a realistic failure in the dependency install step, for example by
configuring the host without network access or by adding an invalid dependency so that
`npm ci` fails when run in the app directory used by `_npm_rebuild` at
`scripts/runewager_redeploy.sh:146-175`.

2. Execute `sudo ./scripts/runewager_redeploy.sh` so `main()` at `326-361` runs
`_npm_rebuild` at `351-353` after stopping the service and killing the port.

3. Inside `_npm_rebuild`, the pipeline `nice -n 19 ionice -c 3 npm ci --prefer-offline
2>&1 | tail -5 | sed 's/^/ /'` at `170-172` exits with a non-zero status due to the `npm
ci` error, but the function unconditionally prints `ok "npm ci complete"` at `173` and
then executes `popd` at `174`, returning success (exit code 0) from `_npm_rebuild`.

4. `main()` continues to `_install_guard`, `_start_bot_service`, and `_health_check` at
`351-356` without any indication that dependencies failed to rebuild, so operators reading
the redeploy output see a "npm ci complete" message even though the install actually
failed.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** scripts/runewager_redeploy.sh
**Line:** 168:174
**Comment:**
	*Logic Error: The `npm ci` step always logs a successful completion and returns success even if the `npm`/`ionice` pipeline fails, which can mislead operators and cause the redeploy to proceed with a broken or incomplete dependency install; checking the pipeline exit code ensures failures are correctly reported and propagated.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

}

# ─────────────────────────────── STEP 4: INSTALL GUARD ───────────────────────

_install_guard() {
step "4/8 — Install / restart rw_cpu_guard"

if [[ ! -x "$GUARD_SCRIPT" ]]; then
fail "Guard script not found or not executable: $GUARD_SCRIPT"
return 1
fi

if (( DRY_RUN )); then
warn "[DRY-RUN] would run: $GUARD_SCRIPT --install"
return 0
fi

"$GUARD_SCRIPT" --install 2>&1 | tail -8 | sed 's/^/ /'
ok "rw_cpu_guard installed and running"
}

# ─────────────────────────────── STEP 5: START BOT ───────────────────────────

_start_bot_service() {
step "5/8 — Start ${BOT_SERVICE} systemd service"

if ! systemctl list-unit-files --type=service 2>/dev/null | grep -q "^${BOT_SERVICE}.service"; then
warn "No systemd unit for '${BOT_SERVICE}' — attempting prod-run.sh instead"
local app; app=$(_app)
if [[ -x "$app/prod-run.sh" ]]; then
if (( DRY_RUN )); then
warn "[DRY-RUN] would run: $app/prod-run.sh"
else
bash "$app/prod-run.sh" 2>&1 | tail -10 | sed 's/^/ /'
fi
else
fail "prod-run.sh not found either — start the bot manually"
fi
return 0
fi

run "Start ${BOT_SERVICE}" systemctl start "$BOT_SERVICE"
}

# ─────────────────────────────── STEP 6: HEALTH CHECK ────────────────────────

_health_check() {
step "6/8 — Health check (waiting up to ${HEALTH_TIMEOUT}s)"

if (( DRY_RUN )); then
warn "[DRY-RUN] would poll ${HEALTH_URL}"
return 0
fi

local elapsed=0
while (( elapsed < HEALTH_TIMEOUT )); do
local http_code
http_code=$(curl -sS --max-time 3 -o /dev/null -w "%{http_code}" "$HEALTH_URL" 2>/dev/null) || true
if [[ "$http_code" == "200" ]]; then
ok "Bot healthy at ${HEALTH_URL} (${elapsed}s)"
# Print health payload
curl -sS --max-time 3 "$HEALTH_URL" 2>/dev/null | python3 -m json.tool 2>/dev/null \
| head -20 | sed 's/^/ /' || true
return 0
fi
sleep 3
elapsed=$(( elapsed + 3 ))
echo -n " waiting... (${elapsed}s / ${http_code}) "
printf "\r"
done

echo ""
fail "Bot did not respond at ${HEALTH_URL} within ${HEALTH_TIMEOUT}s"
warn "Check: journalctl -u ${BOT_SERVICE} -n 30 --no-pager"
return 1
}

# ─────────────────────────────── STEP 7: BACKGROUND FORENSICS ────────────────

_start_forensics() {
step "7/8 — Start background forensics trace (5 min)"

if (( SKIP_FORENSICS )); then
warn "Forensics skipped (--skip-forensics)"
return 0
fi

if (( DRY_RUN )); then
warn "[DRY-RUN] would run: $GUARD_SCRIPT --forensics=300 &"
return 0
fi

if [[ ! -x "$GUARD_SCRIPT" ]]; then
warn "Guard script not found — skipping forensics"
return 0
fi

"$GUARD_SCRIPT" --forensics=300 >> /var/log/rw_cpu_forensics_deploy.log 2>&1 &
local fpid=$!
ok "Forensics trace running in background (PID ${fpid})"
ok "Log: /var/log/rw_cpu_forensics_deploy.log"
ok "Data: /root/rw_cpu_forensics/"
}

# ─────────────────────────────── STEP 8: SUMMARY ─────────────────────────────

_summary() {
step "8/8 — Final status summary"
echo ""
echo -e " ${_bold}Service states:${_reset}"

for svc in "$BOT_SERVICE" "rw_cpu_guard"; do
local state
state=$(systemctl is-active "$svc" 2>/dev/null) || state="not-found"
if [[ "$state" == "active" ]]; then
ok " ${svc}: ${state}"
else
fail " ${svc}: ${state}"
fi
done

echo ""
echo -e " ${_bold}Port ${BOT_PORT}:${_reset}"
local port_pids; port_pids=$(lsof -t -i :"$BOT_PORT" 2>/dev/null) || true
if [[ -n "$port_pids" ]]; then
ok " In use by PID(s): $port_pids"
ps -p "$port_pids" -o pid=,cmd= 2>/dev/null | sed 's/^/ /' || true
else
warn " Nothing on port ${BOT_PORT} — bot may still be starting"
fi

echo ""
echo -e " ${_bold}Load avg:${_reset} $(cat /proc/loadavg)"
echo -e " ${_bold}Memory:${_reset} $(free -m | awk 'NR==2{printf "used=%sMB free=%sMB\n",$3,$4}')"
echo ""

echo -e " ${_bold}Last guard actions:${_reset}"
grep "\[ACT \]" /var/log/rw_cpu_guard.log 2>/dev/null | tail -5 \
| sed 's/^/ /' || echo " (no guard log yet)"

echo ""
echo -e " ${_bold}Bot log tail (last 8 lines):${_reset}"
journalctl -u "$BOT_SERVICE" -n 8 --no-pager 2>/dev/null \
| sed 's/^/ /' || echo " (no journal entries)"

echo ""
hdr "REDEPLOY COMPLETE"
}

# ─────────────────────────────── MAIN ────────────────────────────────────────

main() {
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
--skip-forensics) SKIP_FORENSICS=1 ;;
--help|-h)
grep '^#' "${BASH_SOURCE[0]}" | head -25 | sed 's/^# *//'
exit 0
;;
*)
echo "Unknown option: $arg" >&2
exit 1
;;
esac
done

hdr "Runewager Nuclear Redeploy — $(date)"
echo -e " App dir : $(_app)"
echo -e " Guard : $GUARD_SCRIPT"
echo -e " Dry-run : $( (( DRY_RUN )) && echo YES || echo no )"
echo ""

_check_root
_check_app_dir

_stop_bot_service
_kill_port
_npm_rebuild
_install_guard
_start_bot_service
_health_check || true # non-fatal — bot may be slow to start; summary will show state
_start_forensics
_summary
}

main "$@"
Loading