diff --git a/prod-run.sh b/prod-run.sh index 6f0f2b7..3f8f093 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -1,552 +1,338 @@ #!/usr/bin/env bash +# ============================================================= +# Runewager — Production Runner +# +# Safe to re-run at any time (fully idempotent). +# Resolves its own location — works from any working directory. +# Must be run as root (or a user with write access to /etc/). +# ============================================================= set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$SCRIPT_DIR" -INDEX_FILE="$PROJECT_DIR/index.js" -ENV_FILE="$PROJECT_DIR/.env" -LOG_DIR="$PROJECT_DIR/logs" -BOT_LOG="$LOG_DIR/runewager.log" -LEGACY_BOT_LOG="$PROJECT_DIR/runewager.log" -CRASH_LOG="$LOG_DIR/crash.log" -SYSTEMD_STDOUT_LOG="$LOG_DIR/runewager-1.log" -SERVICE_FILE="/etc/systemd/system/runewager.service" -LOGROTATE_FILE="/etc/logrotate.d/runewager" -CRON_LINE='0 3 * * * /usr/sbin/logrotate -f /etc/logrotate.d/runewager' -HEALTH_LOG_MAX_AGE_MINUTES=10 - -say() { - printf '[runewager] %s\n' "$*" -} - -warn() { - printf '[runewager][warn] %s\n' "$*" >&2 -} - -err() { - printf '[runewager][error] %s\n' "$*" >&2 -} - -ensure_dir() { - [ -d "$1" ] || mkdir -p "$1" -} - -ensure_file() { - [ -f "$1" ] || : > "$1" -} - -ensure_log_paths() { - ensure_dir "$LOG_DIR" - ensure_file "$BOT_LOG" - ensure_file "$CRASH_LOG" - ensure_file "$SYSTEMD_STDOUT_LOG" - - if [ ! -f "$LEGACY_BOT_LOG" ]; then - : > "$LEGACY_BOT_LOG" - fi -} - - -get_bot_pid() { - pgrep -f "node .*index\.js" | head -n 1 || true -} - -log_age_seconds() { - local file="$1" - if [ ! -f "$file" ]; then - echo -1 - return - fi - local now mtime - now="$(date +%s)" - mtime="$(stat -c %Y "$file" 2>/dev/null || echo 0)" - echo $((now - mtime)) -} - -print_health_summary() { - local pid - pid="$(get_bot_pid)" - - if [ -n "$pid" ]; then - say "✅ Bot is running (pid: $pid)" - else - err "❌ Bot is not running" - return - fi - - local active_log="$BOT_LOG" - if [ ! -s "$active_log" ] && [ -s "$LEGACY_BOT_LOG" ]; then - active_log="$LEGACY_BOT_LOG" - fi - - local age - age="$(log_age_seconds "$active_log")" - if [ -s "$active_log" ] && [ "$age" -ge 0 ] && [ "$age" -le $((HEALTH_LOG_MAX_AGE_MINUTES * 60)) ]; then - say "✅ Health check passed: recent logs are present (${age}s old) at $active_log" - return - fi - - if [ -s "$active_log" ] && [ "$age" -gt $((HEALTH_LOG_MAX_AGE_MINUTES * 60)) ]; then - warn "Bot process is running but logs are stale (${age}s old) at $active_log" - else - warn "Bot process is running but no recent logs were found" - fi - - warn "If /start is unresponsive, restart with: pkill -f 'node .*index\.js' && bash $PROJECT_DIR/prod-run.sh" -} - -fix_working_directory() { - if [ "$(pwd)" != "$PROJECT_DIR" ]; then - say "Switching working directory to $PROJECT_DIR" - cd "$PROJECT_DIR" - fi -} - -fix_merge_conflicts() { - if command -v rg >/dev/null 2>&1 && rg -n '^(<<<<<<<|=======|>>>>>>>)' "$PROJECT_DIR"/*.js "$PROJECT_DIR"/*.sh "$PROJECT_DIR"/*.md >/dev/null 2>&1; then - warn "Merge conflict markers detected. Auto-fix is unsafe; please resolve conflict markers manually." - fi -} - -fix_lockfiles() { - if [ -f "$PROJECT_DIR/package-lock.json" ] && [ -f "$PROJECT_DIR/yarn.lock" ]; then - warn "Both package-lock.json and yarn.lock detected. Removing yarn.lock to keep npm workflow deterministic." - rm -f "$PROJECT_DIR/yarn.lock" - fi - if [ -f "$PROJECT_DIR/package-lock.json" ] && [ -f "$PROJECT_DIR/pnpm-lock.yaml" ]; then - warn "Both package-lock.json and pnpm-lock.yaml detected. Removing pnpm-lock.yaml to keep npm workflow deterministic." - rm -f "$PROJECT_DIR/pnpm-lock.yaml" - fi -} +# --------------------------------------------------------- +# 0) Resolve project directory & cd into it +# --------------------------------------------------------- +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$PROJECT_DIR" -fix_git_remote_and_branch() { - if command -v git >/dev/null 2>&1 && git -C "$PROJECT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - local expected='https://github.com/gamblecodezcom/Runewager.git' - local origin='' - origin="$(git -C "$PROJECT_DIR" remote get-url origin 2>/dev/null || true)" - if [ -n "$origin" ] && [ "$origin" != "$expected" ] && [ "$origin" != 'git@github.com:gamblecodezcom/Runewager.git' ]; then - warn "Fixing git origin URL" - git -C "$PROJECT_DIR" remote set-url origin "$expected" || warn "Unable to set git origin URL" - fi - - local current_branch='' - current_branch="$(git -C "$PROJECT_DIR" branch --show-current 2>/dev/null || true)" - if [ -n "$current_branch" ] && [ "$current_branch" != 'main' ]; then - warn "Switching to main branch" - git -C "$PROJECT_DIR" fetch origin main || warn "Could not fetch origin/main" - git -C "$PROJECT_DIR" checkout -f main || warn "Could not checkout main" - git -C "$PROJECT_DIR" reset --hard origin/main || warn "Could not hard reset to origin/main" - fi - fi -} - -fix_node_modules() { - if [ ! -d "$PROJECT_DIR/node_modules" ]; then - if [ -f "$PROJECT_DIR/package-lock.json" ]; then - say "Installing dependencies with npm ci --omit=dev" - npm ci --omit=dev +APP_NAME="runewager" +LOG_DIR="$PROJECT_DIR/logs" +MAIN_LOG="$LOG_DIR/${APP_NAME}.log" +ERROR_LOG="$LOG_DIR/${APP_NAME}-error.log" +SERVICE_FILE="/etc/systemd/system/${APP_NAME}.service" +LOGROTATE_FILE="/etc/logrotate.d/${APP_NAME}" + +say() { printf '[%s] %s\n' "$APP_NAME" "$*"; } +warn() { printf '[%s][warn] %s\n' "$APP_NAME" "$*" >&2; } +err() { printf '[%s][error] %s\n' "$APP_NAME" "$*" >&2; } + +say "Starting production runner" +say "Project directory: $PROJECT_DIR" +say "Running as: $(id -un) (uid=$(id -u) gid=$(id -g))" + +# --------------------------------------------------------- +# 1) Pull latest code from origin main [FIRST — before anything] +# --------------------------------------------------------- +say "Pulling latest code from origin main..." +if git -C "$PROJECT_DIR" pull origin main 2>&1; then + say "Git pull successful" +else + warn "Git pull failed — continuing with local copy" +fi + +# --------------------------------------------------------- +# 2) Ensure required runtime directories & log files exist +# (logs/ and data/ are in .gitignore so they won't exist +# on a fresh clone — we always create them here) +# --------------------------------------------------------- +mkdir -p "$LOG_DIR" +mkdir -p "$PROJECT_DIR/data" +mkdir -p "$PROJECT_DIR/data/backups" +touch "$MAIN_LOG" "$ERROR_LOG" # create if missing; safe on existing files + +say "Ensured runtime directories and log files exist" + +# --------------------------------------------------------- +# 3) Validate Node.js & npm are available +# --------------------------------------------------------- +if ! command -v node >/dev/null 2>&1; then + err "node is not on PATH — install Node.js >= 20 and retry" + exit 1 +fi +if ! command -v npm >/dev/null 2>&1; then + err "npm is not on PATH — install Node.js >= 20 and retry" + exit 1 +fi +say "Node: $(node --version) npm: $(npm --version)" + +# --------------------------------------------------------- +# 4) Ensure .env exists +# --------------------------------------------------------- +if [[ ! -f "$PROJECT_DIR/.env" ]]; then + if [[ -f "$PROJECT_DIR/.env.example" ]]; then + say "Creating .env from .env.example" + cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env" else - say "Installing dependencies with npm install --omit=dev (package-lock.json missing)" - npm install --omit=dev - fi - fi -} - -fix_onboarding_assets() { - local missing=0 - for image in discord_code_generation.png discord_verify.png promo_entry.png; do - if [ ! -f "$PROJECT_DIR/images/$image" ]; then - warn "Missing onboarding asset: images/$image" - missing=1 + warn ".env missing and no .env.example found — creating blank .env" + touch "$PROJECT_DIR/.env" fi - done - if [ "$missing" -eq 1 ]; then - warn "Onboarding image assets are missing; restore files in images/." - fi -} - -normalize_env() { - if [ ! -f "$ENV_FILE" ]; then - if [ -f "$PROJECT_DIR/.env.example" ]; then - say "Creating .env from .env.example" - cp "$PROJECT_DIR/.env.example" "$ENV_FILE" +fi + +# --------------------------------------------------------- +# 5) Alphabetize .env keys (strips blanks lines and comments) +# --------------------------------------------------------- +if [[ -f "$PROJECT_DIR/.env" ]]; then + awk 'NF && $0 !~ /^\s*#/{print}' "$PROJECT_DIR/.env" \ + | LC_ALL=C sort -u > "$PROJECT_DIR/.env.tmp" || true + if [[ -s "$PROJECT_DIR/.env.tmp" ]]; then + mv "$PROJECT_DIR/.env.tmp" "$PROJECT_DIR/.env" + say "Alphabetized .env keys" else - warn ".env and .env.example are missing; creating blank .env" - : > "$ENV_FILE" - fi - fi - - local keys=( - AFFILIATE_SOURCE - ADMIN_IDS - BOT_TOKEN - TELEGRAM_BOT_TOKEN - DISCORD_CODE_GENERATION_IMAGE_URL - DISCORD_VERIFY_IMAGE_URL - MAX_ANALYTICS_EVENTS - MAX_ONBOARDING_STEPS_HISTORY - MINI_APP_CLAIM_URL - MINI_APP_PLAY_URL - MINI_APP_PROFILE_URL - PORT - PROMO_AMOUNT_SC - PROMO_BONUS_RULE - PROMO_CLAIM_LIMIT - PROMO_CODE - PROMO_ENTRY_IMAGE_URL - RW_DISCORD_JOIN - RW_DISCORD_LINK - RW_DISCORD_SUPPORT - ) - - local k - for k in "${keys[@]}"; do - if ! grep -Eq "^${k}=" "$ENV_FILE"; then - warn "Adding missing .env key placeholder: $k" - printf '%s=\n' "$k" >> "$ENV_FILE" + rm -f "$PROJECT_DIR/.env.tmp" fi - done - - if command -v awk >/dev/null 2>&1; then - awk 'NF && $0 !~ /^\s*#/{print}' "$ENV_FILE" | LC_ALL=C sort -u > "$ENV_FILE.tmp" || true - if [ -s "$ENV_FILE.tmp" ]; then - mv "$ENV_FILE.tmp" "$ENV_FILE" - say "Alphabetized .env keys" +fi + +# --------------------------------------------------------- +# 6) Install node_modules if missing +# --------------------------------------------------------- +if [[ ! -d "$PROJECT_DIR/node_modules" ]]; then + say "node_modules missing — installing dependencies" + if [[ -f "$PROJECT_DIR/package-lock.json" ]]; then + npm ci --omit=dev else - rm -f "$ENV_FILE.tmp" - fi - fi -} - -validate_env_values() { - local required=( - BOT_TOKEN - TELEGRAM_BOT_TOKEN - RW_DISCORD_JOIN - RW_DISCORD_LINK - RW_DISCORD_SUPPORT - MINI_APP_CLAIM_URL - MINI_APP_PLAY_URL - MINI_APP_PROFILE_URL - PROMO_AMOUNT_SC - PROMO_BONUS_RULE - PROMO_CLAIM_LIMIT - PROMO_CODE - PORT - ) - local missing=0 - - local k - for k in "${required[@]}"; do - if ! grep -Eq "^${k}=[^[:space:]].*" "$ENV_FILE"; then - warn "Required .env value is empty: $k" - missing=1 + npm install --omit=dev fi - done - - # Check Discord image URLs specifically. - for k in DISCORD_CODE_GENERATION_IMAGE_URL DISCORD_VERIFY_IMAGE_URL PROMO_ENTRY_IMAGE_URL; do - if ! grep -Eq "^${k}=https?://" "$ENV_FILE"; then - warn "Missing or invalid URL for $k" - missing=1 - fi - done - - if [ "$missing" -eq 1 ]; then - warn "One or more required env values are missing; update .env before production start." - fi -} - -fix_json_files() { - local json - shopt -s nullglob - for json in "$PROJECT_DIR"/*.json "$PROJECT_DIR"/data/*.json; do - if ! node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$json" >/dev/null 2>&1; then - warn "Invalid JSON detected: $json (auto-fix skipped to avoid data loss)" - fi - done - shopt -u nullglob -} - -fix_entrypoint_and_scripts() { - if [ ! -f "$INDEX_FILE" ]; then - err "index.js is missing and cannot be auto-generated safely" - exit 1 - fi - if [ ! -f "$PROJECT_DIR/package.json" ]; then - err "package.json is missing" - exit 1 - fi - - if ! node -e "const p=require('./package.json');if(!p.scripts||!p.scripts.start)process.exit(1)" >/dev/null 2>&1; then - warn "package.json scripts.start missing; adding scripts.start=node index.js" - node - <<'NODE' -const fs = require('fs'); -const p = JSON.parse(fs.readFileSync('package.json','utf8')); -p.scripts = p.scripts || {}; -p.scripts.start = p.scripts.start || 'node index.js'; -fs.writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n'); -NODE - fi -} - -fix_broken_symlinks() { - if command -v find >/dev/null 2>&1; then - local broken='' - broken="$(find "$PROJECT_DIR" -xtype l 2>/dev/null || true)" - if [ -n "$broken" ]; then - warn "Broken symlinks found; removing" - while IFS= read -r link; do - [ -n "$link" ] && rm -f "$link" - done <<< "$broken" - fi - fi +else + say "node_modules present — skipping install" +fi + +# --------------------------------------------------------- +# 7) Project-scoped PID detection +# Matches ONLY this project's node process — never collides +# with other Node apps running on the same VPS. +# --------------------------------------------------------- +get_bot_pid() { + pgrep -f "node .*${PROJECT_DIR}/index\.js" | head -n 1 || true } -fix_require_and_import_hints() { - if ! node --check "$INDEX_FILE" >/dev/null 2>&1; then - warn "Syntax issues detected in index.js; please fix manually." - fi - - if ! npm ls --omit=dev >/dev/null 2>&1; then - warn "Dependency graph has missing modules; running npm ci --omit=dev" - npm ci --omit=dev || true - fi -} +PID="$(get_bot_pid)" -ensure_logrotate_config() { - local log_owner="runewager" - local log_group="runewager" - if ! id -u "$log_owner" >/dev/null 2>&1; then - log_owner="$(id -un 2>/dev/null || echo root)" - log_group="$(id -gn 2>/dev/null || echo root)" - fi - - local conf - conf=$(cat < "$LOGROTATE_FILE" - else - warn "No permission to write $LOGROTATE_FILE" - fi - fi - - if [ -f "$LOGROTATE_FILE" ] && ! grep -Fq "$LOG_DIR/runewager*.log" "$LOGROTATE_FILE"; then - warn "Replacing $LOGROTATE_FILE to use detected log directory" - printf '%s\n' "$conf" > "$LOGROTATE_FILE" - fi - - if [ -f "$LOGROTATE_FILE" ] && command -v logrotate >/dev/null 2>&1; then - local logrotate_output='' - if ! logrotate_output="$(logrotate -d "$LOGROTATE_FILE" 2>&1)"; then - warn "logrotate dry-run failed for $LOGROTATE_FILE" - warn "$logrotate_output" +# --------------------------------------------------------- +# 8) Systemd service — create/update if missing or outdated +# --------------------------------------------------------- +ensure_systemd_service() { + # Dynamically resolve node binary path + local node_bin + node_bin="$(command -v node || echo /usr/bin/node)" + + # Use the 'runewager' system user if it exists; otherwise fall back + # to the current user so the service can still be created safely. + local svc_user svc_group + if id -u "$APP_NAME" >/dev/null 2>&1; then + svc_user="$APP_NAME" + svc_group="$APP_NAME" else - say "logrotate config validation passed" + svc_user="$(id -un)" + svc_group="$(id -gn)" + warn "System user '$APP_NAME' not found — using $svc_user:$svc_group for service" fi - fi -} - -ensure_cron_fallback() { - if ! command -v crontab >/dev/null 2>&1; then - warn "crontab is unavailable; skipping cron fallback" - return - fi - - local current - current="$(crontab -l 2>/dev/null || true)" - if ! printf '%s\n' "$current" | grep -Fqx "$CRON_LINE"; then - warn "Adding daily forced logrotate cron fallback" - (printf '%s\n' "$current"; printf '%s\n' "$CRON_LINE") | sed '/^$/N;/^\n$/D' | crontab - - fi -} -ensure_systemd_service() { - local service_user="runewager" - local service_group="runewager" - if ! id -u "$service_user" >/dev/null 2>&1; then - service_user="$(id -un 2>/dev/null || echo root)" - service_group="$(id -gn 2>/dev/null || echo root)" - warn "runewager user missing; using $service_user:$service_group for systemd service" - fi - - local conf - conf=$(cat < "$SERVICE_FILE" - say "Created $SERVICE_FILE" - else - warn "No permission to create $SERVICE_FILE" - return - fi - fi - - local required=( - "WorkingDirectory=$PROJECT_DIR" - "ExecStart=$PROJECT_DIR/prod-run.sh" - "User=$service_user" - "Group=$service_group" - "EnvironmentFile=$PROJECT_DIR/.env" - "StandardOutput=append:$SYSTEMD_STDOUT_LOG" - 'Environment=NODE_ENV=production' - 'Restart=always' - 'RestartSec=5' - 'KillSignal=SIGTERM' - 'TimeoutStopSec=20' - ) - - local missing=0 - local line - for line in "${required[@]}"; do - if ! grep -Fqx "$line" "$SERVICE_FILE"; then - warn "Missing systemd directive: $line" - missing=1 + if [[ ! -f "$SERVICE_FILE" ]]; then + warn "Systemd service missing — creating $SERVICE_FILE" fi - done - if [ "$missing" -eq 1 ] && [ -w "$SERVICE_FILE" ]; then - warn "Replacing $SERVICE_FILE with corrected content" - printf '%s\n' "$conf" > "$SERVICE_FILE" - fi - - if command -v systemctl >/dev/null 2>&1; then - systemctl daemon-reload >/dev/null 2>&1 || true - fi -} - -print_human_errors() { - say "Last 200 lines of $BOT_LOG" - tail -n 200 "$BOT_LOG" || true - - local blob - blob="$(tail -n 200 "$BOT_LOG" 2>/dev/null || true)" - if [ -z "$blob" ] && [ -f "$LEGACY_BOT_LOG" ]; then - blob="$(tail -n 200 "$LEGACY_BOT_LOG" 2>/dev/null || true)" - fi - - say "Human-readable diagnosis" - printf '%s\n' "$blob" | grep -qi 'Cannot find module' && say "Root cause: A Node module is missing or a require path is wrong. Run npm ci and verify relative import paths." - printf '%s\n' "$blob" | grep -qi 'SyntaxError' && say "Root cause: JavaScript syntax is invalid. Review the file/line listed in the log." - printf '%s\n' "$blob" | grep -qiE 'EACCES|permission denied' && say "Root cause: File permissions are too strict for the service user. Fix ownership/permissions for app, logs, and .env." - printf '%s\n' "$blob" | grep -qiE 'ENOENT|no such file or directory' && say "Root cause: A required file/path is missing (env, logs, index.js, or runtime dir)." - printf '%s\n' "$blob" | grep -qiE 'ECONNREFUSED|ETIMEDOUT|getaddrinfo' && say "Root cause: Network or upstream service is unavailable. Check DNS/firewall/connectivity." - printf '%s\n' "$blob" | grep -qiE 'BOT_TOKEN|TELEGRAM_BOT_TOKEN|RW_DISCORD_' && say "Root cause: Required env variables are missing or empty." - - if [ -z "$blob" ]; then - say "No recent logs found; app may not have started yet." - fi -} - -is_bot_running() { - [ -n "$(get_bot_pid)" ] + if [[ -w /etc/systemd/system ]] || [[ -f "$SERVICE_FILE" && -w "$SERVICE_FILE" ]]; then + printf '%s\n' "$conf" > "$SERVICE_FILE" + systemctl daemon-reload + systemctl enable "${APP_NAME}.service" 2>/dev/null || true + say "Systemd service installed and enabled: $SERVICE_FILE" + else + warn "No write access to /etc/systemd/system — re-run as root to install service" + fi } -check_bot_status_twice() { - print_health_summary - sleep 15 - print_health_summary +ensure_systemd_service + +# --------------------------------------------------------- +# 9) Logrotate — create/update with numeric UID/GID +# Avoids "unknown user 'runewager'" errors on systems where +# the runewager system user has not been created. +# --------------------------------------------------------- +ensure_logrotate() { + local uid_num gid_num + uid_num="$(id -u)" + gid_num="$(id -g)" + + local conf + conf=$(cat </dev/null 2>&1 && [ -n "${INVOCATION_ID:-}" ]; then - say "Detected systemd-managed execution; running in foreground for systemd supervision" - exec node "$INDEX_FILE" >> "$BOT_LOG" 2>&1 - fi - - if is_bot_running; then - say "Bot already running; skip new launch" - return - fi - - say "Launching bot in background with nohup" - nohup node "$INDEX_FILE" >> "$BOT_LOG" 2>&1 < /dev/null & - disown || true - sleep 1 - if is_bot_running; then - say "Background launch successful" - else - warn "Background launch failed; inspect $BOT_LOG" - fi -} + if [[ ! -f "$LOGROTATE_FILE" ]]; then + warn "Logrotate config missing — creating $LOGROTATE_FILE" + fi -main() { - fix_working_directory + if [[ -w /etc/logrotate.d ]] || [[ -f "$LOGROTATE_FILE" && -w "$LOGROTATE_FILE" ]]; then + printf '%s\n' "$conf" > "$LOGROTATE_FILE" + say "Logrotate config written: $LOGROTATE_FILE" + else + warn "No write access to /etc/logrotate.d — re-run as root to install logrotate config" + return + fi - if ! command -v node >/dev/null 2>&1; then - err "node is not available on PATH" - exit 1 - fi - if ! command -v npm >/dev/null 2>&1; then - err "npm is not available on PATH" - exit 1 - fi - - ensure_log_paths - ensure_dir "$PROJECT_DIR/data" - ensure_dir "$PROJECT_DIR/data/backups" - - fix_merge_conflicts - fix_lockfiles - fix_git_remote_and_branch - fix_node_modules - fix_entrypoint_and_scripts - fix_broken_symlinks - normalize_env - validate_env_values - fix_json_files - fix_onboarding_assets - fix_require_and_import_hints - ensure_logrotate_config - ensure_cron_fallback - ensure_systemd_service - - check_bot_status_twice - start_background_if_needed - check_bot_status_twice - print_human_errors + # Validate config — capture output, only flag actual errors (suppress debug spam) + local lr_out + lr_out="$(logrotate -d -s /dev/null "$LOGROTATE_FILE" 2>&1)" || true + + if printf '%s\n' "$lr_out" | grep -qi "error"; then + warn "Logrotate validation found errors — adding cron fallback" + local cron_line="0 3 * * * /usr/sbin/logrotate -f $LOGROTATE_FILE # ${APP_NAME}_logrotate" + local current_cron + current_cron="$(crontab -l 2>/dev/null || true)" + if ! printf '%s\n' "$current_cron" | grep -Fq "${APP_NAME}_logrotate"; then + { printf '%s\n' "$current_cron"; printf '%s\n' "$cron_line"; } | crontab - + say "Cron fallback added for logrotate" + else + say "Cron fallback already present — skipping" + fi + else + say "Logrotate config valid" + fi } -main "$@" +ensure_logrotate + +# --------------------------------------------------------- +# 10) Structured diagnostics +# --------------------------------------------------------- +echo "--------------------------------------------------" +say "DIAGNOSTICS" +echo "--------------------------------------------------" + +[[ -d "$PROJECT_DIR" ]] \ + && say "✔ Project directory exists: $PROJECT_DIR" \ + || say "✘ Project directory missing" + +[[ -f "$PROJECT_DIR/.env" ]] \ + && say "✔ .env exists" \ + || say "✘ .env missing" + +[[ -f "$PROJECT_DIR/index.js" ]] \ + && say "✔ index.js exists" \ + || say "✘ index.js missing" + +[[ -f "$SERVICE_FILE" ]] \ + && say "✔ Systemd service: $SERVICE_FILE" \ + || say "✘ Systemd service missing" + +[[ -f "$LOGROTATE_FILE" ]] \ + && say "✔ Logrotate config: $LOGROTATE_FILE" \ + || say "✘ Logrotate config missing" + +if [[ -n "$PID" ]]; then + say "✔ Bot running (PID: $PID)" +else + say "✘ Bot not running" +fi + +echo "--------------------------------------------------" +say "Last 20 lines of $MAIN_LOG" +echo "--------------------------------------------------" +if [[ -s "$MAIN_LOG" ]]; then + tail -n 20 "$MAIN_LOG" +else + say "(no log output yet — app may not have started)" +fi + +# --------------------------------------------------------- +# 11) Start bot if not running +# --------------------------------------------------------- +if [[ -z "$PID" ]]; then + say "Bot not running — starting via systemd" + + if command -v systemctl >/dev/null 2>&1 && [[ -f "$SERVICE_FILE" ]]; then + if systemctl restart "${APP_NAME}.service" 2>&1; then + sleep 3 + PID="$(get_bot_pid)" + else + warn "systemctl restart failed — falling back to nohup launch" + nohup node "$PROJECT_DIR/index.js" \ + >> "$MAIN_LOG" 2>> "$ERROR_LOG" < /dev/null & + disown || true + sleep 3 + PID="$(get_bot_pid)" + fi + else + say "systemctl unavailable or service file missing — launching with nohup" + nohup node "$PROJECT_DIR/index.js" \ + >> "$MAIN_LOG" 2>> "$ERROR_LOG" < /dev/null & + disown || true + sleep 3 + PID="$(get_bot_pid)" + fi +else + say "Bot already running — skipping launch" +fi + +# --------------------------------------------------------- +# 12) Final confirmation +# --------------------------------------------------------- +echo "--------------------------------------------------" +say "FINAL STATUS" +echo "--------------------------------------------------" + +if [[ -n "$PID" ]]; then + say "✔ Bot is running in background (PID: $PID)" +else + say "✘ Bot failed to start — check logs:" + say " Main log: $MAIN_LOG" + say " Error log: $ERROR_LOG" +fi + +SYSTEMD_ACTIVE="$(systemctl is-active "${APP_NAME}.service" 2>/dev/null || echo inactive)" +say "✔ Systemd service: ${SYSTEMD_ACTIVE}" +say "✔ Logrotate installed: $(command -v logrotate >/dev/null 2>&1 && echo yes || echo no)" +say "✔ Logs directory: $LOG_DIR" +say "✔ Main log: $MAIN_LOG" +say "✔ Error log: $ERROR_LOG" + +echo "--------------------------------------------------" +say "Production runner complete" +echo "--------------------------------------------------" diff --git a/runewager.logrotate b/runewager.logrotate index c363d8d..b545253 100644 --- a/runewager.logrotate +++ b/runewager.logrotate @@ -1,11 +1,30 @@ -/var/www/html/Runewager/logs/runewager*.log /var/www/html/Runewager/logs/crash.log /var/www/html/runewager/logs/runewager*.log /var/www/html/runewager/logs/crash.log { +# ============================================================= +# Runewager Bot — logrotate config template +# +# This file is a REFERENCE COPY kept in the repo. +# The authoritative /etc/logrotate.d/runewager is written and +# maintained by prod-run.sh, which substitutes the real log +# paths and numeric UID/GID at runtime. +# +# Numeric UID/GID are used in the `create` directive to avoid +# "unknown user 'runewager'" errors on systems where the +# runewager system user does not exist. +# +# To install manually (substitute 0 0 with your actual uid:gid): +# +# cp runewager.logrotate /etc/logrotate.d/runewager +# logrotate -d /etc/logrotate.d/runewager # dry-run check +# +# Prefer running prod-run.sh instead — it handles everything. +# ============================================================= +/var/www/html/Runewager/logs/runewager.log /var/www/html/Runewager/logs/runewager-error.log { daily - size 5M rotate 7 + size 5M missingok notifempty compress delaycompress copytruncate - create 0640 runewager runewager + create 0640 0 0 } diff --git a/runewager.service b/runewager.service index b1d838d..4e6bf16 100644 --- a/runewager.service +++ b/runewager.service @@ -1,22 +1,39 @@ +# ============================================================= +# Runewager Bot — systemd service template +# +# This file is a REFERENCE COPY kept in the repo. +# The authoritative /etc/systemd/system/runewager.service is +# written and maintained by prod-run.sh, which fills in the +# correct PROJECT_DIR, node binary path, and user at runtime. +# +# To install manually on a VPS where the project lives at +# /var/www/html/Runewager, copy this file: +# +# cp runewager.service /etc/systemd/system/runewager.service +# systemctl daemon-reload +# systemctl enable --now runewager +# +# Prefer running prod-run.sh instead — it handles everything. +# ============================================================= [Unit] -Description=Runewager bot +Description=Runewager Bot After=network-online.target Wants=network-online.target [Service] Type=simple WorkingDirectory=/var/www/html/Runewager -ExecStart=/var/www/html/Runewager/prod-run.sh -User=runewager -Group=runewager +ExecStart=/usr/bin/node /var/www/html/Runewager/index.js +User=root +Group=root EnvironmentFile=/var/www/html/Runewager/.env Environment=NODE_ENV=production Restart=always RestartSec=5 KillSignal=SIGTERM TimeoutStopSec=20 -StandardOutput=append:/var/www/html/Runewager/logs/runewager-1.log -StandardError=inherit +StandardOutput=append:/var/www/html/Runewager/logs/runewager.log +StandardError=append:/var/www/html/Runewager/logs/runewager-error.log [Install] WantedBy=multi-user.target