From 31a43b15c3b9defa882c1ef4abdf16ca7272510f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Mar 2026 12:40:21 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix(ops):=20three=20targeted=20fixes=20?= =?UTF-8?q?=E2=80=94=20MarkdownV2=20escaping,=20CPU=20tracer=20args,=20jq?= =?UTF-8?q?=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rw_cpu_guard.sh: - Add escape_md_v2() helper: escapes all 20 MarkdownV2 special characters (backslash processed first to prevent double-escaping) - tg_alert(): escape the full composed text (prefix + caller message) via escape_md_v2() before sending; switch parse_mode Markdown → MarkdownV2; raw ${short_cmd} can no longer break message formatting or leak syntax - CPU tracer: switch ps format 'cmd' → 'args' so the full executable path and every argument are captured; combined with the existing $5..$NF loop this gives a complete, delimiter-safe command string in every CSV row runewager_redeploy.sh: - _health_check pretty-print cascade: python3 -m json.tool → jq . → raw; diagnostics never fail when neither pretty-printer is installed https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- scripts/runewager_redeploy.sh | 8 +++++-- scripts/rw_cpu_guard.sh | 39 ++++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/scripts/runewager_redeploy.sh b/scripts/runewager_redeploy.sh index 41067b5..4cbe973 100755 --- a/scripts/runewager_redeploy.sh +++ b/scripts/runewager_redeploy.sh @@ -251,10 +251,14 @@ _health_check() { 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 — degrade gracefully if python3 absent + # Pretty-print health payload. Cascade: python3 → jq → raw. local _payload; _payload=$(curl -sS --max-time 3 "$HEALTH_URL" 2>/dev/null) if command -v python3 >/dev/null 2>&1; then - echo "$_payload" | python3 -m json.tool 2>/dev/null | head -20 | sed 's/^/ /' || echo "$_payload" | head -20 | sed 's/^/ /' + echo "$_payload" | python3 -m json.tool 2>/dev/null | head -20 | sed 's/^/ /' \ + || echo "$_payload" | head -20 | sed 's/^/ /' + elif command -v jq >/dev/null 2>&1; then + echo "$_payload" | jq . 2>/dev/null | head -20 | sed 's/^/ /' \ + || echo "$_payload" | head -20 | sed 's/^/ /' else echo "$_payload" | head -20 | sed 's/^/ /' fi diff --git a/scripts/rw_cpu_guard.sh b/scripts/rw_cpu_guard.sh index 907e185..47d405c 100755 --- a/scripts/rw_cpu_guard.sh +++ b/scripts/rw_cpu_guard.sh @@ -223,6 +223,33 @@ _load_env() { # ─────────────────────────────────── TELEGRAM ALERT ────────────────────────── +# Escape all MarkdownV2 special characters so arbitrary text is safe to insert +# into a Telegram MarkdownV2 message. Must be called before building the text +# that is handed to sendMessage. +escape_md_v2() { + local s="$1" + s="${s//\\/\\\\}" # backslash — must come first + s="${s//_/\\_}" + s="${s//\*/\\*}" + s="${s//\[/\\[}" + s="${s//\]/\\]}" + s="${s//\(/\\(}" + s="${s//\)/\\)}" + s="${s//~/\\~}" + s="${s//\`/\\\`}" + s="${s//>/\\>}" + s="${s//#/\\#}" + s="${s//+/\\+}" + s="${s//-/\\-}" + s="${s//=/\\=}" + s="${s//|/\\|}" + s="${s//\{/\\{}" + s="${s//\}/\\}}" + s="${s//./\\.}" + s="${s//!/\\!}" + printf '%s' "$s" +} + tg_alert() { local msg="$1" [[ -z "$TG_BOT_TOKEN" || -z "$TG_ADMIN_IDS" ]] && return 0 @@ -232,6 +259,9 @@ tg_alert() { (( elapsed < _ALERT_COOLDOWN )) && return 0 _LAST_ALERT_TS=$now + # Escape the full text (prefix + caller message) for MarkdownV2 + local safe_text; safe_text=$(escape_md_v2 "🛡 [rw_cpu_guard] ${msg}") + IFS=',' read -ra _ids <<< "$TG_ADMIN_IDS" local id for id in "${_ids[@]}"; do @@ -240,8 +270,8 @@ tg_alert() { curl -sS --max-time 6 \ "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \ --data-urlencode "chat_id=${id}" \ - --data-urlencode "text=🛡 [rw_cpu_guard] ${msg}" \ - -d "parse_mode=Markdown" \ + --data-urlencode "text=${safe_text}" \ + -d "parse_mode=MarkdownV2" \ >/dev/null 2>&1 || true done } @@ -650,7 +680,10 @@ _run_forensics() { # ── 1-second CPU tracer (catches sub-8s spikes the guard daemon may miss) ─ echo "timestamp,pid,ppid,cpu%,mem%,cmd" > "$trace" ( while true; do - ps -eo pid,ppid,pcpu,pmem,cmd --sort=-pcpu --no-headers 2>/dev/null \ + # Use 'args' (not 'cmd') so the full command path and every argument + # are captured. Fields $5..$NF are joined into one value so commands + # with spaces in their paths or arguments are never truncated. + ps -eo pid,ppid,pcpu,pmem,args --sort=-pcpu --no-headers 2>/dev/null \ | awk -v ts="$(date '+%F %T')" '$3+0 > 15 { cmd = ""; for (i=5; i<=NF; i++) cmd = cmd (i>5 ? " " : "") $i; gsub(/,/, ";", cmd); From 6f65e3bebfc908efb64f5fd3cb7805974ef0a24f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Mar 2026 12:49:15 +0000 Subject: [PATCH 2/3] fix(ops): csv header typo, dedup print helper, real fallback on format failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rw_cpu_guard.sh: - CSV header: cmd → args to match the ps field now being recorded runewager_redeploy.sh: - Extract _show_payload() helper: captures formatter output into a variable, then falls back to raw only when output is empty/non-zero; fixes the silent-failure bug where sed's exit code masked python3/jq failures so the || fallback never triggered - Replace three duplicated 'echo | head -20 | sed' blocks with a single call to _show_payload(), keeping the python3 → jq → raw cascade https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- scripts/runewager_redeploy.sh | 20 +++++++++++++++----- scripts/rw_cpu_guard.sh | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts/runewager_redeploy.sh b/scripts/runewager_redeploy.sh index 4cbe973..f99aeb4 100755 --- a/scripts/runewager_redeploy.sh +++ b/scripts/runewager_redeploy.sh @@ -252,15 +252,25 @@ _health_check() { if [[ "$http_code" == "200" ]]; then ok "Bot healthy at ${HEALTH_URL} (${elapsed}s)" # Pretty-print health payload. Cascade: python3 → jq → raw. + # _show_payload pipes TEXT through FORMATTER; falls back to raw if + # the formatter exits non-zero or produces no output. + _show_payload() { + local text="$1" formatter="$2" + local formatted + formatted=$(printf '%s' "$text" | $formatter 2>/dev/null) + if [[ -n "$formatted" ]]; then + printf '%s\n' "$formatted" | head -20 | sed 's/^/ /' + else + printf '%s\n' "$text" | head -20 | sed 's/^/ /' + fi + } local _payload; _payload=$(curl -sS --max-time 3 "$HEALTH_URL" 2>/dev/null) if command -v python3 >/dev/null 2>&1; then - echo "$_payload" | python3 -m json.tool 2>/dev/null | head -20 | sed 's/^/ /' \ - || echo "$_payload" | head -20 | sed 's/^/ /' + _show_payload "$_payload" "python3 -m json.tool" elif command -v jq >/dev/null 2>&1; then - echo "$_payload" | jq . 2>/dev/null | head -20 | sed 's/^/ /' \ - || echo "$_payload" | head -20 | sed 's/^/ /' + _show_payload "$_payload" "jq ." else - echo "$_payload" | head -20 | sed 's/^/ /' + printf '%s\n' "$_payload" | head -20 | sed 's/^/ /' fi return 0 fi diff --git a/scripts/rw_cpu_guard.sh b/scripts/rw_cpu_guard.sh index 47d405c..6ea0338 100755 --- a/scripts/rw_cpu_guard.sh +++ b/scripts/rw_cpu_guard.sh @@ -678,7 +678,7 @@ _run_forensics() { fi # ── 1-second CPU tracer (catches sub-8s spikes the guard daemon may miss) ─ - echo "timestamp,pid,ppid,cpu%,mem%,cmd" > "$trace" + echo "timestamp,pid,ppid,cpu%,mem%,args" > "$trace" ( while true; do # Use 'args' (not 'cmd') so the full command path and every argument # are captured. Fields $5..$NF are joined into one value so commands From 02a8e702726ed8fb58379a6146270e467e408cbc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Mar 2026 12:55:12 +0000 Subject: [PATCH 3/3] fix(redeploy): stop legacy runewager-bot service + fix false not-found status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs exposed by the production redeploy on 2026-03-01: 1. Port-conflict crash loop: runewager-bot.service (previously active) has Restart=always / RestartSec=5. After _kill_port freed port 3000 the legacy service restarted ~5 s later and reclaimed the port. When prod-run.sh then started runewager.service it immediately failed with EADDRINUSE → exit 1 crash loop. _stop_bot_service now also stops known legacy service names (runewager-bot, runewager_bot) before the port-kill step so they cannot race back to life and steal the port. 2. Status display masked real service state as "not-found": systemctl is-active exits non-zero for failed/inactive/activating too, so the old `state=$(…) || state="not-found"` showed every non-active state as "not-found" — hiding that the service was actually in "failed". Now the string printed by is-active is captured directly; "not-found" is only set when the command produces empty output (unit truly unknown). https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- scripts/runewager_redeploy.sh | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/runewager_redeploy.sh b/scripts/runewager_redeploy.sh index f99aeb4..ac7540d 100755 --- a/scripts/runewager_redeploy.sh +++ b/scripts/runewager_redeploy.sh @@ -121,6 +121,17 @@ _stop_bot_service() { else warn "${BOT_SERVICE} service was not running (OK)" fi + + # Stop legacy service names so they can't restart and steal port ${BOT_PORT} + # while this redeploy is in progress. Failures are non-fatal. + local _legacy + for _legacy in "runewager-bot" "runewager_bot"; do + if [[ "$_legacy" != "$BOT_SERVICE" ]] \ + && systemctl is-active --quiet "$_legacy" 2>/dev/null; then + warn "Legacy service '${_legacy}' is running — stopping to prevent port conflict" + systemctl stop "$_legacy" 2>/dev/null || true + fi + done } # ─────────────────────────────── STEP 2: KILL PORT ─────────────────────────── @@ -330,7 +341,11 @@ _summary() { for svc in "$BOT_SERVICE" "rw_cpu_guard"; do local state - state=$(systemctl is-active "$svc" 2>/dev/null) || state="not-found" + # systemctl is-active exits non-zero for failed/inactive/unknown too, + # so capture the printed state string; only default to "not-found" when + # the command produces no output at all (unit truly unknown to systemd). + state=$(systemctl is-active "$svc" 2>/dev/null) + [[ -z "$state" ]] && state="not-found" if [[ "$state" == "active" ]]; then ok " ${svc}: ${state}" else