feat(ops): add rw_cpu_guard.sh — nuclear CPU governor for KVM1 - #116
Conversation
Prevents Runewager from being starved by CPU hogs on Hostinger KVM1. Features: - Load-aware 3-zone response (none / soft / hard) with per-zone %CPU thresholds so the guard is only aggressive when the box is hot - Kill ladder: renice+ionice → cgroup-quota → STOP/CONT (3 s) → SIGTERM → SIGKILL - cgroup v1 + v2 auto-detection; configurable CPU quota (default 70%) - Fork-bomb detection via total-process-count ceiling (default 200) - Telegram admin alerts on STOP/CONT and kills (5-min cooldown, reads BOT_TOKEN + ADMIN_IDS from .env automatically) - Duplicate-instance guard via PID file - Atomic state-file updates (mktemp + mv) — no corruption under load - Comprehensive whitelist covering Runewager (all launch forms), PM2, systemd, sshd, cron, fail2ban, interactive shells, and guard itself; children of whitelisted parents are also protected - Modes: daemon (default), --install (writes systemd unit + enables), --status, --dry-run, --once (single scan, great for cron testing) - No `set -e` in daemon loop — a subcommand failure never kills the guard - Falls back to /tmp paths if /var/log isn't writable (non-root runs) https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
|
CodeAnt AI is reviewing your PR. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Reviewer's GuideAdds a standalone bash-based CPU guard daemon (rw_cpu_guard.sh) for Hostinger KVM1 that monitors system load, identifies non-whitelisted CPU hogs, and throttles or kills them via a multi-step kill ladder, with cgroup-based quotas, fork-bomb protection, Telegram alerts, and systemd integration. Sequence diagram for rw_cpu_guard daemon scan and kill laddersequenceDiagram
participant Guard as rw_cpu_guard_daemon
participant ProcFS as procfs_ps
participant CGroup as cgroup_cpu_controller
participant Hog as Target_process
participant Telegram as Telegram_API
Guard->>ProcFS: Read /proc/loadavg
ProcFS-->>Guard: Current_load
Guard->>Guard: Determine load_zone (none|soft|hard)
Guard->>ProcFS: List /proc PIDs
ProcFS-->>Guard: PIDs
Guard->>Guard: _check_fork_bomb (kill recent non-whitelisted if over limit)
Guard->>ProcFS: ps -eo pid,pcpu,cmd --sort=-pcpu
ProcFS-->>Guard: Process_list
loop For each process
Guard->>ProcFS: Read /proc/pid/cmdline
ProcFS-->>Guard: full_cmd
Guard->>Guard: _is_whitelisted(full_cmd, pid)?
alt Whitelisted
Guard->>Guard: Skip process
else Not whitelisted and cpu >= threshold
Guard->>Guard: strikes = _get_strikes(pid) + 1
Guard->>Guard: _set_strikes(pid, strikes)
Guard->>CGroup: _apply_cgroup(pid)
alt strikes == 1 (strike1)
Guard->>Hog: renice +15
Guard->>Hog: ionice class 3
Guard->>Guard: Log STRIKE1
else strikes == 2 (strike2)
Guard->>Hog: kill -STOP
Guard->>Guard: sleep 3s
Guard->>Hog: kill -CONT
Guard->>Guard: Log STRIKE2 STOP/CONT
Guard->>Telegram: send STOP/CONT alert
Telegram-->>Guard: HTTP 200 (ignored on failure)
else strikes >= 3 (termination)
Guard->>Hog: kill -TERM
Guard->>Guard: sleep 2s
Guard->>Hog: kill -KILL (if still alive)
Guard->>Guard: Log KILLED
Guard->>Telegram: send SIGTERM/SIGKILL alert
Telegram-->>Guard: HTTP 200 (ignored on failure)
end
end
end
Guard->>Guard: sleep SCAN_INTERVAL
Guard->>Guard: Next _scan_once() iteration
Flow diagram for rw_cpu_guard main modes and daemon scan loopflowchart TD
A[Start rw_cpu_guard.sh] --> B[Parse CLI args]
B -->|--install| M[Mode install]
B -->|--status| N[Mode status]
B -->|--once| O[Mode once]
B -->|no special mode| P[Mode daemon]
M --> M1[Check root user]
M1 -->|not root| M2[Print error and exit]
M1 -->|root| M3[Write systemd unit file]
M3 --> M4[systemctl daemon-reload, enable, restart]
M4 --> Z[Exit]
N --> N1[Print load and config]
N1 --> N2[Show PID file status]
N2 --> N3[Show systemd state and strikes]
N3 --> N4[Show recent log lines]
N4 --> Z
subgraph Daemon_Setup
D1[_ensure_log]
D2[Ensure STATE_FILE exists]
D3[Duplicate-instance check via PID_FILE]
D4[trap _cleanup on EXIT/INT/TERM]
D5[_load_env from .env]
D6[_setup_cgroup]
D7[Log startup banner]
end
O --> Daemon_Setup
P --> Daemon_Setup
D7 -->|mode once| S1[_scan_once]
S1 --> Z
D7 -->|mode daemon| L1[Daemon loop]
subgraph Scan_Once
S2[Read /proc/loadavg]
S2 --> S3[Compute load zone none/soft/hard]
S3 --> S4[_clear_dead_pids]
S4 --> S5[_check_fork_bomb]
S5 --> S6[Select CPU threshold by zone]
S6 --> S7[ps -eo pid,pcpu,cmd --sort=-pcpu]
S7 --> S8{For each process}
S8 -->|cpu < threshold or <1%| S9[Skip process]
S9 --> S8
S8 -->|candidate| S10[Read full cmdline from /proc/pid/cmdline]
S10 --> S11{_is_whitelisted?}
S11 -->|yes| S12[Log PROTECTED and skip]
S12 --> S8
S11 -->|no| S13[_get_strikes + 1, _set_strikes]
S13 --> S14[_apply_cgroup pid]
S14 --> S15{Strike count}
S15 -->|1| S16[renice +15, ionice class 3]
S16 --> S8
S15 -->|2| S17[STOP 3s then CONT, log]
S17 --> S18[Optional Telegram STOP/CONT alert]
S18 --> S8
S15 -->|>=3| S19[TERM, wait, then KILL if needed]
S19 --> S20[Log and Telegram kill alert]
S20 --> S8
S8 -->|done| S21[Return from _scan_once]
end
L1 --> S2
S21 --> L2[sleep SCAN_INTERVAL]
L2 --> L1
Z[End]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review infoConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughA new Bash daemon script is introduced to monitor and regulate CPU load on a VPS by protecting a Runewager bot through multi-zone load classification, process throttling, and forceful termination when necessary, with cgroup enforcement, Telegram alerting, and fork-bomb detection. Changes
Sequence Diagram(s)sequenceDiagram
participant Daemon as CPU Guard Daemon
participant Kernel as Linux Kernel
participant ProcFS as /proc Filesystem
participant CGroup as cgroup Subsystem
participant Telegram as Telegram API
participant StateFile as State File
rect rgba(100, 150, 255, 0.5)
Note over Daemon: Scan Cycle Start
end
Daemon->>Kernel: Read CPU load average
Daemon->>ProcFS: Enumerate running processes
Daemon->>StateFile: Load strike counters
rect rgba(200, 150, 100, 0.5)
Note over Daemon: Per-Process Evaluation
end
loop For each non-whitelisted process
Daemon->>ProcFS: Get process CPU%, memory, cmdline
Daemon->>Daemon: Check load zone & assign threshold
alt Threshold exceeded (Strike 1)
Daemon->>Kernel: renice/ionice process
Daemon->>StateFile: Increment strike counter
else Strike 2
Daemon->>Kernel: Send SIGSTOP
Daemon->>Telegram: Alert admin (optional)
Daemon->>Kernel: Send SIGCONT (after delay)
Daemon->>StateFile: Increment strike counter
else Strike 3+
Daemon->>Kernel: Send SIGTERM
Daemon->>Telegram: Alert admin (optional)
alt SIGTERM failed
Daemon->>Kernel: Send SIGKILL
end
Daemon->>StateFile: Reset strike counter
end
end
rect rgba(150, 200, 100, 0.5)
Note over Daemon: Fork-Bomb Check
end
Daemon->>ProcFS: Count total processes
alt Process count > threshold
Daemon->>Kernel: Kill excess non-whitelisted processes
Daemon->>Telegram: Alert fork-bomb event
end
rect rgba(100, 100, 200, 0.5)
Note over Daemon: Cleanup & Persist
end
Daemon->>StateFile: Clean dead PIDs, atomic write
Daemon->>Daemon: Sleep until next scan interval
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Sequence DiagramThe new script runs as a daemon, samples system load, scans processes, protects whitelisted processes, and applies a graduated "kill ladder" (throttle → pause → terminate) with optional cgroup throttling and Telegram alerts for severe actions. sequenceDiagram
participant rw_cpu_guard
participant Host as System (/proc, ps)
participant CGroup
participant Process
participant Telegram
rw_cpu_guard->>Host: read /proc/loadavg and ps (process list)
rw_cpu_guard->>rw_cpu_guard: determine load zone (none/soft/hard) and threshold
rw_cpu_guard->>Process: inspect top CPU processes
alt Process is whitelisted
rw_cpu_guard-->>Process: skip protection
else Non-whitelisted hog above threshold
rw_cpu_guard->>CGroup: best-effort add PID to cgroup (throttle)
rw_cpu_guard->>Process: renice + ionice (soft throttle)
rw_cpu_guard->>Process: STOP (3s) then CONT (hard throttle)
rw_cpu_guard->>Process: SIGTERM → if still alive → SIGKILL
rw_cpu_guard->>Telegram: send alert for STOP/CONT or kills (cooldown)
end
Generated by CodeAnt AI |
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The whitelist matching is very broad (simple substrings over the full cmdline, e.g. "bash", "node.*index", "/var/www") which can accidentally protect unexpected or malicious processes; consider tightening this by matching against the executable name (comm) or using anchored patterns and/or explicit path checks.
- The Telegram alert messages interpolate unescaped
short_cmdinto Markdown text, which can break formatting or fail if the command contains Markdown metacharacters; consider escaping or switching toparse_mode=MarkdownV2with proper escaping, or usingparse_mode=HTML/no parse mode. - In
_load_env, the simpleKEY=VALUEparsing will misbehave on lines withexport, spaces around=, or embedded=characters; if you expect such cases in.env, consider using a more robust parser (e.g.set -a; source .env; set +ain a subshell and then copying only the needed vars).
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The whitelist matching is very broad (simple substrings over the full cmdline, e.g. "bash", "node.*index", "/var/www") which can accidentally protect unexpected or malicious processes; consider tightening this by matching against the executable name (comm) or using anchored patterns and/or explicit path checks.
- The Telegram alert messages interpolate unescaped `short_cmd` into Markdown text, which can break formatting or fail if the command contains Markdown metacharacters; consider escaping or switching to `parse_mode=MarkdownV2` with proper escaping, or using `parse_mode=HTML`/no parse mode.
- In `_load_env`, the simple `KEY=VALUE` parsing will misbehave on lines with `export`, spaces around `=`, or embedded `=` characters; if you expect such cases in `.env`, consider using a more robust parser (e.g. `set -a; source .env; set +a` in a subshell and then copying only the needed vars).
## Individual Comments
### Comment 1
<location path="scripts/rw_cpu_guard.sh" line_range="143-146" />
<code_context>
+ "runc"
+
+ # Interactive shells and common admin tools
+ "bash"
+ "zsh"
+ "sh "
+ "sh\t"
+ "-bash"
+ "-zsh"
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Very broad shell whitelist entries risk protecting malicious or runaway shell workloads.
Patterns like `"bash"`, `"zsh"`, `"sh "`, `"sh\t"`, etc. effectively exempt *all* shell processes from throttling, including CPU-hungry scripts or fork bombs, which defeats this guard in realistic failure modes. Consider narrowing the logic by e.g. only whitelisting shells whose parent is `sshd`/`systemd` (interactive sessions) or by detecting the presence of a TTY (`ps`/`/proc`), so interactive admin use stays protected while non-interactive shell workloads remain subject to the guard.
Suggested implementation:
```
# Interactive shells and common admin tools
#
# NOTE:
# Avoid broadly whitelisting shells by name (e.g. "bash", "zsh", "sh") because
# that would exempt non-interactive workloads and runaway scripts from throttling.
# Interactive shells should be detected via parent/TTY inspection instead
# (e.g. shell with parent "sshd"/"systemd" and an attached TTY).
# Only keep explicitly interactive admin helpers here.
"tmux"
"screen"
"vim"
"nano"
```
To fully implement the behavior described in your review comment, you should also:
1. Introduce an `is_interactive_session` helper (or equivalent logic) that:
- Checks whether the candidate process has an attached TTY (e.g. by inspecting `/proc/$pid/fd/0` or using `ps -o tty= -p "$pid"`).
- Optionally verifies the parent process name is `sshd`, `systemd`, or another trusted session manager (via `/proc/$pid/stat` or `ps -o ppid= -p "$pid"` followed by a lookup of the parent’s command name).
2. Integrate this helper into the throttling decision path:
- When deciding whether to throttle a process, first test the whitelist by `comm`.
- If not whitelisted by `comm`, call `is_interactive_session "$pid"` and skip throttling when it returns true.
3. Ensure any existing callers that relied on `"bash"`/`"zsh"` name-based whitelisting are updated to use the new interactive-session logic so that legitimate interactive admin shells remain protected while non-interactive shell workloads are still subject to the guard.
</issue_to_address>
### Comment 2
<location path="scripts/rw_cpu_guard.sh" line_range="76" />
<code_context>
+# Files
+LOG_FILE="${LOG_FILE:-/var/log/${SCRIPT_NAME}.log}"
+STATE_FILE="${STATE_FILE:-/tmp/${SCRIPT_NAME}.state}"
+PID_FILE="${PID_FILE:-/tmp/${SCRIPT_NAME}.pid}"
+
+# cgroup CPU quota applied to throttled (non-whitelisted) processes
</code_context>
<issue_to_address>
**🚨 issue (security):** Storing the PID file in /tmp allows spoofing or DoS by unprivileged users.
Because any user can create or modify `/tmp/${SCRIPT_NAME}.pid`, a local attacker can pre-populate it with an arbitrary PID so the script believes an instance is already running and exits, or otherwise disrupts restarts. Prefer a root-only directory such as `/run/${SCRIPT_NAME}.pid` or `/var/run/${SCRIPT_NAME}.pid` when appropriate, and validate that the PID file owner and the process it references (e.g., via `/proc/$pid/cmdline`) match this script before trusting it.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| "bash" | ||
| "zsh" | ||
| "sh " | ||
| "sh\t" |
There was a problem hiding this comment.
suggestion (bug_risk): Very broad shell whitelist entries risk protecting malicious or runaway shell workloads.
Patterns like "bash", "zsh", "sh ", "sh\t", etc. effectively exempt all shell processes from throttling, including CPU-hungry scripts or fork bombs, which defeats this guard in realistic failure modes. Consider narrowing the logic by e.g. only whitelisting shells whose parent is sshd/systemd (interactive sessions) or by detecting the presence of a TTY (ps//proc), so interactive admin use stays protected while non-interactive shell workloads remain subject to the guard.
Suggested implementation:
# Interactive shells and common admin tools
#
# NOTE:
# Avoid broadly whitelisting shells by name (e.g. "bash", "zsh", "sh") because
# that would exempt non-interactive workloads and runaway scripts from throttling.
# Interactive shells should be detected via parent/TTY inspection instead
# (e.g. shell with parent "sshd"/"systemd" and an attached TTY).
# Only keep explicitly interactive admin helpers here.
"tmux"
"screen"
"vim"
"nano"
To fully implement the behavior described in your review comment, you should also:
- Introduce an
is_interactive_sessionhelper (or equivalent logic) that:- Checks whether the candidate process has an attached TTY (e.g. by inspecting
/proc/$pid/fd/0or usingps -o tty= -p "$pid"). - Optionally verifies the parent process name is
sshd,systemd, or another trusted session manager (via/proc/$pid/statorps -o ppid= -p "$pid"followed by a lookup of the parent’s command name).
- Checks whether the candidate process has an attached TTY (e.g. by inspecting
- Integrate this helper into the throttling decision path:
- When deciding whether to throttle a process, first test the whitelist by
comm. - If not whitelisted by
comm, callis_interactive_session "$pid"and skip throttling when it returns true.
- When deciding whether to throttle a process, first test the whitelist by
- Ensure any existing callers that relied on
"bash"/"zsh"name-based whitelisting are updated to use the new interactive-session logic so that legitimate interactive admin shells remain protected while non-interactive shell workloads are still subject to the guard.
| # Files | ||
| LOG_FILE="${LOG_FILE:-/var/log/${SCRIPT_NAME}.log}" | ||
| STATE_FILE="${STATE_FILE:-/tmp/${SCRIPT_NAME}.state}" | ||
| PID_FILE="${PID_FILE:-/tmp/${SCRIPT_NAME}.pid}" |
There was a problem hiding this comment.
🚨 issue (security): Storing the PID file in /tmp allows spoofing or DoS by unprivileged users.
Because any user can create or modify /tmp/${SCRIPT_NAME}.pid, a local attacker can pre-populate it with an arbitrary PID so the script believes an instance is already running and exits, or otherwise disrupts restarts. Prefer a root-only directory such as /run/${SCRIPT_NAME}.pid or /var/run/${SCRIPT_NAME}.pid when appropriate, and validate that the PID file owner and the process it references (e.g., via /proc/$pid/cmdline) match this script before trusting it.
Nitpicks 🔍
|
| if (( strikes == 1 )); then | ||
| # ── Strike 1: Graceful throttle ────────────────────────────────────── | ||
| _do renice +15 -p "$pid" | ||
| _do ionice -c 3 -p "$pid" | ||
| act "STRIKE1 | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}" | ||
|
|
||
| elif (( strikes == 2 )); then | ||
| # ── Strike 2: Hard pause — STOP 3s then CONT ───────────────────────── | ||
| _do kill -STOP "$pid" | ||
| sleep 3 | ||
| _do kill -CONT "$pid" | ||
| act "STRIKE2 | PID=${pid} CPU=${cpu_pct}% zone=${zone} STOP/CONT cmd=${short_cmd}" | ||
| tg_alert "⚠️ STOP/CONT throttle — PID=${pid} CPU=${cpu_pct}% zone=${zone} — \`${short_cmd}\`" | ||
|
|
||
| else | ||
| # ── Strike 3+: Termination (SIGTERM then SIGKILL) ──────────────────── | ||
| _do kill -TERM "$pid" | ||
| sleep 2 | ||
| if _is_alive "$pid"; then | ||
| _do kill -KILL "$pid" | ||
| act "KILLED(SIGKILL) | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}" | ||
| tg_alert "💀 SIGKILL — PID=${pid} CPU=${cpu_pct}% zone=${zone} — \`${short_cmd}\`" | ||
| else | ||
| act "KILLED(SIGTERM) | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}" | ||
| tg_alert "💀 SIGTERM — PID=${pid} CPU=${cpu_pct}% zone=${zone} — \`${short_cmd}\`" | ||
| fi | ||
| fi |
There was a problem hiding this comment.
Suggestion: The configurable MAX_STRIKES value is never actually used in the kill ladder, so changing it in the environment has no effect and processes are always terminated from the third strike onward; this is a logic bug and can be fixed by incorporating MAX_STRIKES into the decision of when to move from STOP/CONT throttling to termination so the configured strike count is honored. [logic error]
Severity Level: Major ⚠️
- ⚠️ MAX_STRIKES configuration has no effect on kills.
- ⚠️ Admins cannot extend strike ladder before termination.
- ⚠️ Guard may kill heavy tasks earlier than intended.| if (( strikes == 1 )); then | |
| # ── Strike 1: Graceful throttle ────────────────────────────────────── | |
| _do renice +15 -p "$pid" | |
| _do ionice -c 3 -p "$pid" | |
| act "STRIKE1 | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}" | |
| elif (( strikes == 2 )); then | |
| # ── Strike 2: Hard pause — STOP 3s then CONT ───────────────────────── | |
| _do kill -STOP "$pid" | |
| sleep 3 | |
| _do kill -CONT "$pid" | |
| act "STRIKE2 | PID=${pid} CPU=${cpu_pct}% zone=${zone} STOP/CONT cmd=${short_cmd}" | |
| tg_alert "⚠️ STOP/CONT throttle — PID=${pid} CPU=${cpu_pct}% zone=${zone} — \`${short_cmd}\`" | |
| else | |
| # ── Strike 3+: Termination (SIGTERM then SIGKILL) ──────────────────── | |
| _do kill -TERM "$pid" | |
| sleep 2 | |
| if _is_alive "$pid"; then | |
| _do kill -KILL "$pid" | |
| act "KILLED(SIGKILL) | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}" | |
| tg_alert "💀 SIGKILL — PID=${pid} CPU=${cpu_pct}% zone=${zone} — \`${short_cmd}\`" | |
| else | |
| act "KILLED(SIGTERM) | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}" | |
| tg_alert "💀 SIGTERM — PID=${pid} CPU=${cpu_pct}% zone=${zone} — \`${short_cmd}\`" | |
| fi | |
| fi | |
| if (( strikes == 1 )); then | |
| # ── Strike 1: Graceful throttle ────────────────────────────────────── | |
| _do renice +15 -p "$pid" | |
| _do ionice -c 3 -p "$pid" | |
| act "STRIKE1 | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}" | |
| elif (( strikes < MAX_STRIKES )); then | |
| # ── Intermediate strikes: Hard pause — STOP 3s then CONT ───────────── | |
| _do kill -STOP "$pid" | |
| sleep 3 | |
| _do kill -CONT "$pid" | |
| act "STRIKE${strikes} | PID=${pid} CPU=${cpu_pct}% zone=${zone} STOP/CONT cmd=${short_cmd}" | |
| tg_alert "⚠️ STOP/CONT throttle — PID=${pid} CPU=${cpu_pct}% zone=${zone} — \`${short_cmd}\`" | |
| else | |
| # ── Final strike: Termination (SIGTERM then SIGKILL) ───────────────── | |
| _do kill -TERM "$pid" | |
| sleep 2 | |
| if _is_alive "$pid"; then | |
| _do kill -KILL "$pid" | |
| act "KILLED(SIGKILL) | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}" | |
| tg_alert "💀 SIGKILL — PID=${pid} CPU=${cpu_pct}% zone=${zone} — \`${short_cmd}\`" | |
| else | |
| act "KILLED(SIGTERM) | PID=${pid} CPU=${cpu_pct}% zone=${zone} cmd=${short_cmd}" | |
| tg_alert "💀 SIGTERM — PID=${pid} CPU=${cpu_pct}% zone=${zone} — \`${short_cmd}\`" | |
| fi | |
| fi |
Steps of Reproduction ✅
1. Set `MAX_STRIKES=5` in the environment and start the guard with
`scripts/rw_cpu_guard.sh` so `MAX_STRIKES` is read into config at
`scripts/rw_cpu_guard.sh:67` and reported by `_show_status()` at lines 542-550.
2. Launch a CPU-intensive, non-whitelisted process (for example `yes > /dev/null &`) so
that `_scan_once()` at `scripts/rw_cpu_guard.sh:444-488` repeatedly identifies its PID as
exceeding the per-zone CPU threshold.
3. Each scan calls `_handle_hog()` at `scripts/rw_cpu_guard.sh:373-416`, which increments
the strike count for that PID via `_get_strikes` / `_set_strikes` (lines 332-347), then
dispatches behavior based only on `strikes == 1`, `strikes == 2`, or `else` at lines
389-403.
4. Despite `MAX_STRIKES=5`, observe from the logs (written by `act()` at lines 186-189)
and from the process list that on the third invocation of `_handle_hog()` (when `strikes
== 3`), the code enters the `else` branch at lines 403-415, sends SIGTERM/SIGKILL, and
kills the process, demonstrating that the configured `MAX_STRIKES` value is ignored and
termination always starts at strike 3.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** scripts/rw_cpu_guard.sh
**Line:** 389:415
**Comment:**
*Logic Error: The configurable `MAX_STRIKES` value is never actually used in the kill ladder, so changing it in the environment has no effect and processes are always terminated from the third strike onward; this is a logic bug and can be fixed by incorporating `MAX_STRIKES` into the decision of when to move from STOP/CONT throttling to termination so the configured strike count is honored.
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.|
CodeAnt AI finished reviewing your PR. |
The ladder previously hardcoded 3 tiers (strike==1 / strike==2 / else), so MAX_STRIKES had no effect — termination always fired on the 3rd hit regardless of configuration. Fix: replace `strikes == 2` with `strikes < MAX_STRIKES` so the STOP/CONT pause repeats for strikes 2..(MAX_STRIKES-1) and termination only triggers at strike >= MAX_STRIKES. All log and Telegram messages now include the current strike / total (e.g. "STRIKE3/5") for clarity. Reported by CodeAnt AI review of PR #116. https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
User description
Prevents Runewager from being starved by CPU hogs on Hostinger KVM1.
Features:
%CPU thresholds so the guard is only aggressive when the box is hot
reads BOT_TOKEN + ADMIN_IDS from .env automatically)
systemd, sshd, cron, fail2ban, interactive shells, and guard itself;
children of whitelisted parents are also protected
--status, --dry-run, --once (single scan, great for cron testing)
set -ein daemon loop — a subcommand failure never kills the guardhttps://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4
Summary by Sourcery
Add a dedicated CPU guard daemon script to protect the Runewager bot from CPU starvation on Hostinger KVM1 by monitoring load and throttling or killing non-whitelisted hog processes.
New Features:
Enhancements:
Deployment:
CodeAnt-AI Description
Protect Runewager bot from CPU starvation with a dedicated rw_cpu_guard daemon
What Changed
Impact
✅ Fewer Runewager CPU starvations✅ Clearer admin alerts on process throttles and kills✅ Shorter recovery time after fork-bomb events💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.
Summary by CodeRabbit