commit f687aa8f23ef2b14836864e83e33bd744418075a Author: Ryan T. Murphy Date: Thu Jan 22 18:46:43 2026 -0500 Replace with B2 archive contents diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc87244 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.log +*.bak +*.swp +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..53a4140 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 UpfrontOps + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7370d7d --- /dev/null +++ b/README.md @@ -0,0 +1,299 @@ +# Runaway Process Killer + +Automatic protection against runaway CPU and RAM processes on Ubuntu servers. Prevents hosting provider throttling by killing processes that pin CPU to 95%+ for extended periods or exhaust available memory. + +## Overview + +| Protection | Tool | Trigger | Action | +|------------|------|---------|--------| +| CPU | monit | 95%+ CPU for 5 minutes | Kill all processes matching top CPU consumer | +| RAM | earlyoom | <5% free memory | Kill highest memory consumer | +| Orphan Claude | monit | Every 15 minutes | Kill Claude processes with no TTY or PPID=1 | + +## Requirements + +- Ubuntu 20.04+ (tested on 24.04 LTS) +- Root access +- ~6MB RAM overhead total + +## Quick Install + +```bash +curl -fsSL https://git.upfrontops.cloud/upfrontops/runaway-process-killer/raw/branch/main/install.sh | sudo bash +``` + +Or clone and run: + +```bash +git clone https://git.upfrontops.cloud/upfrontops/runaway-process-killer.git +cd runaway-process-killer +sudo ./install.sh +``` + +## Manual Installation + +### 1. Install earlyoom (RAM Protection) + +```bash +sudo apt update && sudo apt install -y earlyoom +``` + +Edit `/etc/default/earlyoom`: +```bash +EARLYOOM_ARGS="-m 5 -s 5 --avoid '(^|/)(init|systemd|sshd)$' -r 60" +``` + +Enable and start: +```bash +sudo systemctl enable earlyoom && sudo systemctl restart earlyoom +``` + +### 2. Install monit (CPU Protection) + +```bash +sudo apt install -y monit +``` + +Edit `/etc/monit/monitrc`, change daemon interval: +``` +set daemon 60 +``` + +Enable HTTP interface (required for `monit status`): +``` +set httpd port 2812 and + use address localhost + allow localhost + allow admin:monit +``` + +Create `/etc/monit/conf.d/cpu-killer`: +``` +check system $HOST + if cpu usage > 95% for 5 cycles then exec "/usr/local/bin/kill-top-cpu.sh" +``` + +Create `/usr/local/bin/kill-top-cpu.sh`: +```bash +#!/bin/bash +# Kill the process tree using the most CPU (excluding critical ones) + +# Find the top CPU consumer (excluding protected and transient processes) +TOP_LINE=$(ps -eo pid,comm,%cpu --sort=-%cpu | grep -v -E '(PID|systemd|sshd|monit|earlyoom|bash|ps|awk|grep|head)' | head -1) +TARGET_PID=$(echo "$TOP_LINE" | awk '{print $1}') +COMM=$(echo "$TOP_LINE" | awk '{print $2}') + +if [ -n "$TARGET_PID" ] && [ -n "$COMM" ]; then + logger "monit cpu-killer: Killing all '$COMM' processes (detected high CPU on PID $TARGET_PID)" + # Kill all processes with this command name + pkill -9 -x "$COMM" +fi +``` + +Make executable and enable: +```bash +sudo chmod +x /usr/local/bin/kill-top-cpu.sh +sudo systemctl enable monit && sudo systemctl restart monit +``` + +## Configuration + +### CPU Threshold Timing + +Edit `/etc/monit/conf.d/cpu-killer` to change timing: + +| Cycles | Time (at 60s interval) | +|--------|------------------------| +| 2 | 2 minutes | +| 5 | 5 minutes (default) | +| 10 | 10 minutes | +| 30 | 30 minutes | + +### RAM Threshold + +Edit `/etc/default/earlyoom`: + +| Setting | Meaning | +|---------|---------| +| `-m 5` | Kill when free RAM < 5% | +| `-m 10` | Kill when free RAM < 10% | +| `-s 5` | Kill when free swap < 5% | + +### Protected Processes + +**earlyoom** protects (via `--avoid`): +- init, systemd, sshd + +**kill-top-cpu.sh** protects (via grep exclusion): +- systemd, sshd, monit, earlyoom, bash, ps, awk, grep, head + +To add more protected processes, edit the grep pattern in `/usr/local/bin/kill-top-cpu.sh`. + +## Monitoring + +### Check Status + +```bash +# earlyoom status +sudo systemctl status earlyoom + +# monit status +sudo monit status + +# Combined check +sudo ./scripts/status.sh +``` + +### View Logs + +```bash +# earlyoom logs +journalctl -u earlyoom -f + +# monit logs +tail -f /var/log/monit.log + +# Kill events +journalctl | grep -i "cpu-killer\|earlyoom\|orphan-claude" +``` + +## Testing + +### Test CPU Killer + +```bash +# Install stress tool +sudo apt install -y stress + +# For quick testing, temporarily set to 2 cycles in /etc/monit/conf.d/cpu-killer +# then reload: sudo monit reload + +# Start CPU stress (will be killed after threshold) +stress --cpu 4 --timeout 300 +``` + +### Test RAM Killer + +```bash +# This will be killed quickly by earlyoom +stress --vm 4 --vm-bytes 4G --vm-keep --timeout 120 +``` + +### Test Orphan Claude Killer + +```bash +# Run the detection script manually to see what it would find +sudo /usr/local/bin/kill-orphan-claude.sh + +# Check logs for any kills +journalctl | grep orphan-claude-killer +``` + +## Uninstall + +```bash +sudo ./uninstall.sh +``` + +Or manually: + +```bash +sudo systemctl stop earlyoom monit +sudo systemctl disable earlyoom monit +sudo apt remove -y earlyoom monit +sudo rm -f /usr/local/bin/kill-top-cpu.sh +sudo rm -f /usr/local/bin/kill-orphan-claude.sh +sudo rm -f /etc/monit/conf.d/cpu-killer +sudo rm -f /etc/monit/conf.d/orphan-claude-killer +``` + +## Resource Overhead + +| Component | RAM | CPU | Disk | +|-----------|-----|-----|------| +| earlyoom | ~2MB | Negligible (adaptive polling) | 77KB | +| monit | ~3-4MB | ~28ms per 60s cycle | 1MB | +| kill script | 0 (runs only when triggered) | Milliseconds | <1KB | + +**Total: ~6MB RAM, essentially 0% CPU during normal operation** + +## How It Works + +### Orphan Claude Detection (monit) + +A Claude process is considered orphaned if: +- Its controlling TTY is `?` (no terminal attached), OR +- Its parent PID is 1 (adopted by init) + +1. monit runs the orphan detection script every 15 cycles (15 minutes with 60s daemon interval) +2. Script finds all `claude` processes via `pgrep -x claude` +3. For each process, checks TTY (`ps -o tty=`) and PPID (`ps -o ppid=`) +4. If orphaned, kills the process tree (children first, then parent) +5. Logs details to syslog including PID, reason, start time, CPU%, and memory% + +### CPU Protection (monit) + +1. monit checks system CPU every 60 seconds +2. If CPU > 95% for 5 consecutive checks (5 minutes), executes kill script +3. Kill script identifies the process using most CPU +4. Kills ALL processes with that command name (handles multi-worker processes) +5. Logs the action to syslog + +### RAM Protection (earlyoom) + +1. earlyoom monitors available memory (adaptive polling - more frequent when memory is low) +2. When free memory drops below 5%, sends SIGTERM to highest memory consumer +3. If process doesn't exit, sends SIGKILL at 2.5% threshold +4. Protected processes (init, systemd, sshd) are never killed + +## Troubleshooting + +### monit not triggering + +```bash +# Check monit is running +sudo systemctl status monit + +# Check config syntax +sudo monit -t + +# Check monit log +tail -f /var/log/monit.log + +# Verify CPU threshold is being detected +sudo monit status | grep cpu +``` + +### earlyoom not killing + +```bash +# Check earlyoom is running +sudo systemctl status earlyoom + +# Check configuration +cat /etc/default/earlyoom + +# Watch real-time +journalctl -u earlyoom -f +``` + +### Kill script not working + +```bash +# Test manually +sudo /usr/local/bin/kill-top-cpu.sh + +# Check script is executable +ls -la /usr/local/bin/kill-top-cpu.sh + +# Check for errors +bash -x /usr/local/bin/kill-top-cpu.sh +``` + +## License + +MIT License - Use freely, no warranty. + +## Author + +Created for UpfrontOps infrastructure management. diff --git a/config/cpu-killer.conf b/config/cpu-killer.conf new file mode 100644 index 0000000..174de37 --- /dev/null +++ b/config/cpu-killer.conf @@ -0,0 +1,2 @@ +check system $HOST + if cpu usage > 95% for 5 cycles then exec "/usr/local/bin/kill-top-cpu.sh" diff --git a/config/earlyoom.conf b/config/earlyoom.conf new file mode 100644 index 0000000..85b75e5 --- /dev/null +++ b/config/earlyoom.conf @@ -0,0 +1,21 @@ +# Default settings for earlyoom. This file is sourced by /bin/sh from +# /etc/init.d/earlyoom or by systemd from earlyoom.service. + +# Options to pass to earlyoom +EARLYOOM_ARGS="-m 5 -s 5 --avoid '(^|/)(init|systemd|sshd)$' -r 60" + +# Examples: + +# Print memory report every second instead of every minute +# EARLYOOM_ARGS="-r 1" + +# Available minimum memory 5% +# EARLYOOM_ARGS="-m 5" + +# Available minimum memory 15% and free minimum swap 5% +# EARLYOOM_ARGS="-m 15 -s 5" + +# Avoid killing processes whose name matches this regexp +# EARLYOOM_ARGS="--avoid '(^|/)(init|X|sshd|firefox)$'" + +# See more at `earlyoom -h' diff --git a/config/orphan-claude-killer.conf b/config/orphan-claude-killer.conf new file mode 100644 index 0000000..bae3096 --- /dev/null +++ b/config/orphan-claude-killer.conf @@ -0,0 +1,3 @@ +check program orphan-claude-killer with path "/usr/local/bin/kill-orphan-claude.sh" + every 15 cycles + if status != 0 then alert diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..82184dc --- /dev/null +++ b/install.sh @@ -0,0 +1,182 @@ +#!/bin/bash +set -e + +# Runaway Process Killer - Installation Script +# Protects against CPU and RAM runaway processes + +echo "============================================" +echo "Runaway Process Killer - Installation" +echo "============================================" +echo "" + +# Check root +if [ "$EUID" -ne 0 ]; then + echo "Error: Please run as root (sudo ./install.sh)" + exit 1 +fi + +# Detect script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Configuration +CPU_CYCLES="${CPU_CYCLES:-5}" # 5 minutes default (5 cycles * 60s) +RAM_THRESHOLD="${RAM_THRESHOLD:-5}" # 5% free memory threshold +SWAP_THRESHOLD="${SWAP_THRESHOLD:-5}" # 5% free swap threshold + +echo "Configuration:" +echo " CPU kill threshold: ${CPU_CYCLES} minutes of 95%+ CPU" +echo " RAM kill threshold: <${RAM_THRESHOLD}% free memory" +echo "" + +# Install packages +echo "[1/8] Installing packages..." +apt-get update -qq +apt-get install -y -qq earlyoom monit + +# Configure earlyoom +echo "[2/8] Configuring earlyoom (RAM protection)..." +cat > /etc/default/earlyoom << EOF +# earlyoom configuration - managed by runaway-process-killer +EARLYOOM_ARGS="-m ${RAM_THRESHOLD} -s ${SWAP_THRESHOLD} --avoid '(^|/)(init|systemd|sshd)\$' -r 60" +EOF + +# Configure monit daemon interval +echo "[3/8] Configuring monit daemon..." +if grep -q "^set daemon" /etc/monit/monitrc; then + sed -i 's/^set daemon.*/set daemon 60/' /etc/monit/monitrc +else + sed -i '1i set daemon 60' /etc/monit/monitrc +fi + +# Enable monit HTTP interface (required for monit status) +if ! grep -q "^set httpd port 2812" /etc/monit/monitrc; then + cat >> /etc/monit/monitrc << 'EOF' + +# HTTP interface for monit status command +set httpd port 2812 and + use address localhost + allow localhost + allow admin:monit +EOF +fi + +# Create CPU killer config +echo "[4/8] Creating CPU monitoring rule..." +cat > /etc/monit/conf.d/cpu-killer << EOF +check system \$HOST + if cpu usage > 95% for ${CPU_CYCLES} cycles then exec "/usr/local/bin/kill-top-cpu.sh" +EOF + +# Create kill script +echo "[5/8] Installing CPU kill script..." +cat > /usr/local/bin/kill-top-cpu.sh << 'EOF' +#!/bin/bash +# Kill the process tree using the most CPU (excluding critical ones) + +# Find the top CPU consumer (excluding protected and transient processes) +TOP_LINE=$(ps -eo pid,comm,%cpu --sort=-%cpu | grep -v -E '(PID|systemd|sshd|monit|earlyoom|bash|ps|awk|grep|head)' | head -1) +TARGET_PID=$(echo "$TOP_LINE" | awk '{print $1}') +COMM=$(echo "$TOP_LINE" | awk '{print $2}') + +if [ -n "$TARGET_PID" ] && [ -n "$COMM" ]; then + logger "monit cpu-killer: Killing all '$COMM' processes (detected high CPU on PID $TARGET_PID)" + # Kill all processes with this command name + pkill -9 -x "$COMM" +fi +EOF +chmod +x /usr/local/bin/kill-top-cpu.sh + +# Create orphan Claude killer script +echo "[6/8] Installing orphan Claude killer script..." +cat > /usr/local/bin/kill-orphan-claude.sh << 'EOF' +#!/bin/bash +# Kill orphaned Claude processes (no TTY or parent is init) +# An orphaned Claude process is one where: +# - TTY is "?" (no terminal attached), OR +# - Parent PID is 1 (adopted by init) + +LOG_TAG="orphan-claude-killer" + +# Find all claude processes +CLAUDE_PIDS=$(pgrep -x claude 2>/dev/null) + +if [ -z "$CLAUDE_PIDS" ]; then + exit 0 +fi + +for PID in $CLAUDE_PIDS; do + # Get TTY and PPID for this process + PROC_INFO=$(ps -o tty=,ppid= -p "$PID" 2>/dev/null) + + if [ -z "$PROC_INFO" ]; then + # Process already gone + continue + fi + + PROC_TTY=$(echo "$PROC_INFO" | awk '{print $1}') + PARENT_PID=$(echo "$PROC_INFO" | awk '{print $2}') + + ORPHANED=false + REASON="" + + # Check if TTY is "?" (no terminal) + if [ "$PROC_TTY" = "?" ]; then + ORPHANED=true + REASON="no TTY attached" + fi + + # Check if parent PID is 1 (adopted by init) + if [ "$PARENT_PID" = "1" ]; then + ORPHANED=true + REASON="parent is init (PPID=1)" + fi + + if [ "$ORPHANED" = true ]; then + # Get process start time for logging + START_TIME=$(ps -o lstart= -p "$PID" 2>/dev/null) + CPU=$(ps -o %cpu= -p "$PID" 2>/dev/null) + MEM=$(ps -o %mem= -p "$PID" 2>/dev/null) + + logger "$LOG_TAG: Killing orphaned claude process PID=$PID ($REASON) started='$START_TIME' cpu=$CPU% mem=$MEM%" + + # Kill the process tree (claude may have child processes) + pkill -9 -P "$PID" 2>/dev/null + kill -9 "$PID" 2>/dev/null + fi +done +EOF +chmod +x /usr/local/bin/kill-orphan-claude.sh + +# Create orphan Claude killer config +echo "[7/8] Creating orphan Claude monitoring rule..." +cat > /etc/monit/conf.d/orphan-claude-killer << 'EOF' +check program orphan-claude-killer with path "/usr/local/bin/kill-orphan-claude.sh" + every 15 cycles + if status != 0 then alert +EOF + +# Enable and start services +echo "[8/8] Enabling and starting services..." +systemctl enable earlyoom monit +systemctl restart earlyoom monit + +# Verify +echo "" +echo "============================================" +echo "Installation Complete!" +echo "============================================" +echo "" +echo "Status:" +systemctl is-active earlyoom && echo " earlyoom: running" || echo " earlyoom: FAILED" +systemctl is-active monit && echo " monit: running" || echo " monit: FAILED" +echo "" +echo "Configuration:" +echo " CPU: Kill after ${CPU_CYCLES} minutes of 95%+ usage" +echo " RAM: Kill when free memory < ${RAM_THRESHOLD}%" +echo " Orphan Claude: Check every 15 minutes" +echo "" +echo "Commands:" +echo " sudo monit status # Check monit status" +echo " journalctl -u earlyoom # View earlyoom logs" +echo " tail -f /var/log/monit.log # View monit logs" +echo "" diff --git a/scripts/kill-orphan-claude.sh b/scripts/kill-orphan-claude.sh new file mode 100755 index 0000000..38e97d9 --- /dev/null +++ b/scripts/kill-orphan-claude.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Kill orphaned Claude processes (no TTY or parent is init) +# An orphaned Claude process is one where: +# - TTY is "?" (no terminal attached), OR +# - Parent PID is 1 (adopted by init) + +LOG_TAG="orphan-claude-killer" + +# Find all claude processes +CLAUDE_PIDS=$(pgrep -x claude 2>/dev/null) + +if [ -z "$CLAUDE_PIDS" ]; then + exit 0 +fi + +for PID in $CLAUDE_PIDS; do + # Get TTY and PPID for this process + PROC_INFO=$(ps -o tty=,ppid= -p "$PID" 2>/dev/null) + + if [ -z "$PROC_INFO" ]; then + # Process already gone + continue + fi + + PROC_TTY=$(echo "$PROC_INFO" | awk '{print $1}') + PARENT_PID=$(echo "$PROC_INFO" | awk '{print $2}') + + ORPHANED=false + REASON="" + + # Check if TTY is "?" (no terminal) + if [ "$PROC_TTY" = "?" ]; then + ORPHANED=true + REASON="no TTY attached" + fi + + # Check if parent PID is 1 (adopted by init) + if [ "$PARENT_PID" = "1" ]; then + ORPHANED=true + REASON="parent is init (PPID=1)" + fi + + if [ "$ORPHANED" = true ]; then + # Get process start time for logging + START_TIME=$(ps -o lstart= -p "$PID" 2>/dev/null) + CPU=$(ps -o %cpu= -p "$PID" 2>/dev/null) + MEM=$(ps -o %mem= -p "$PID" 2>/dev/null) + + logger "$LOG_TAG: Killing orphaned claude process PID=$PID ($REASON) started='$START_TIME' cpu=$CPU% mem=$MEM%" + + # Kill the process tree (claude may have child processes) + pkill -9 -P "$PID" 2>/dev/null + kill -9 "$PID" 2>/dev/null + fi +done diff --git a/scripts/kill-top-cpu.sh b/scripts/kill-top-cpu.sh new file mode 100755 index 0000000..861024a --- /dev/null +++ b/scripts/kill-top-cpu.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Kill the process tree using the most CPU (excluding critical ones) + +# Find the top CPU consumer (excluding protected and transient processes) +TOP_LINE=$(ps -eo pid,comm,%cpu --sort=-%cpu | grep -v -E '(PID|systemd|sshd|monit|earlyoom|bash|ps|awk|grep|head)' | head -1) +TARGET_PID=$(echo "$TOP_LINE" | awk '{print $1}') +COMM=$(echo "$TOP_LINE" | awk '{print $2}') + +if [ -n "$TARGET_PID" ] && [ -n "$COMM" ]; then + logger "monit cpu-killer: Killing all '$COMM' processes (detected high CPU on PID $TARGET_PID)" + # Kill all processes with this command name + pkill -9 -x "$COMM" +fi diff --git a/scripts/status.sh b/scripts/status.sh new file mode 100755 index 0000000..bd20b3c --- /dev/null +++ b/scripts/status.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Status check for runaway process killer + +echo "============================================" +echo "Runaway Process Killer - Status" +echo "============================================" +echo "" + +echo "=== Services ===" +echo -n "earlyoom: " +systemctl is-active earlyoom 2>/dev/null || echo "not running" + +echo -n "monit: " +systemctl is-active monit 2>/dev/null || echo "not running" + +echo "" +echo "=== Current System Load ===" +echo -n "CPU: " +top -bn1 | grep "Cpu(s)" | awk '{print $2 "% user, " $4 "% system"}' + +echo -n "RAM: " +free -h | awk '/Mem:/ {printf "%s used / %s total (%.1f%% used)\n", $3, $2, $3/$2*100}' + +echo -n "Swap: " +free -h | awk '/Swap:/ {if ($2 != "0B") printf "%s used / %s total\n", $3, $2; else print "disabled"}' + +echo "" +echo "=== Configuration ===" +if [ -f /etc/monit/conf.d/cpu-killer ]; then + CYCLES=$(grep "for.*cycles" /etc/monit/conf.d/cpu-killer | grep -oP '\d+(?= cycles)') + echo "CPU threshold: ${CYCLES} minutes at 95%+" +else + echo "CPU threshold: NOT CONFIGURED" +fi + +if [ -f /etc/default/earlyoom ]; then + RAM_PCT=$(grep -oP '(?<=-m )\d+' /etc/default/earlyoom) + echo "RAM threshold: <${RAM_PCT}% free memory" +else + echo "RAM threshold: NOT CONFIGURED" +fi + +echo "" +echo "=== Recent Kill Events ===" +echo "CPU kills (last 24h):" +journalctl --since "24 hours ago" 2>/dev/null | grep "cpu-killer" | tail -3 || echo " None" + +echo "" +echo "RAM kills (last 24h):" +journalctl -u earlyoom --since "24 hours ago" 2>/dev/null | grep -E "SIGTERM|SIGKILL" | tail -3 || echo " None" + +echo "" diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..129207e --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# Test script for runaway process killer +# WARNING: This will stress your system! + +set -e + +echo "============================================" +echo "Runaway Process Killer - Test Suite" +echo "============================================" +echo "" +echo "WARNING: This will temporarily stress your CPU and RAM!" +echo "" + +if [ "$EUID" -ne 0 ]; then + echo "Error: Please run as root (sudo ./test.sh)" + exit 1 +fi + +# Check stress is installed +if ! command -v stress &> /dev/null; then + echo "Installing stress tool..." + apt-get install -y -qq stress +fi + +# Backup current config +ORIG_CYCLES=$(grep "for.*cycles" /etc/monit/conf.d/cpu-killer | grep -oP '\d+(?= cycles)') +echo "Current CPU threshold: ${ORIG_CYCLES} cycles" +echo "Temporarily setting to 2 cycles for testing..." + +# Set to 2 cycles for testing +sed -i "s/for ${ORIG_CYCLES} cycles/for 2 cycles/" /etc/monit/conf.d/cpu-killer +monit reload +sleep 2 + +echo "" +echo "=== TEST 1: CPU Protection ===" +echo "Starting CPU stress (expect kill in ~2 minutes)..." +echo "" + +stress --cpu 4 --timeout 300 & +STRESS_PID=$! +sleep 2 + +START=$(date +%s) +while kill -0 $STRESS_PID 2>/dev/null; do + sleep 5 + NOW=$(date +%s) + ELAPSED=$((NOW - START)) + + if [ $ELAPSED -ge 180 ]; then + echo "FAILED: CPU stress not killed after 3 minutes" + kill $STRESS_PID 2>/dev/null || true + break + fi + + if ! pgrep -x stress > /dev/null 2>&1; then + echo "PASSED: CPU stress killed after ${ELAPSED}s" + break + fi + + [ $((ELAPSED % 30)) -eq 0 ] && echo "[${ELAPSED}s] Stress still running..." +done + +sleep 2 + +echo "" +echo "=== TEST 2: RAM Protection ===" +echo "Starting RAM stress (expect quick kill)..." +echo "" + +stress --vm 4 --vm-bytes 4G --vm-keep --timeout 300 & +STRESS_PID=$! +sleep 2 + +START=$(date +%s) +while pgrep -x stress > /dev/null 2>&1; do + sleep 5 + NOW=$(date +%s) + ELAPSED=$((NOW - START)) + + if [ $ELAPSED -ge 120 ]; then + echo "FAILED: RAM stress not killed after 2 minutes" + pkill stress 2>/dev/null || true + break + fi + + if ! pgrep -x stress > /dev/null 2>&1; then + echo "PASSED: RAM stress killed after ${ELAPSED}s" + break + fi + + MEM_FREE=$(free -m | awk '/Mem:/ {print $7}') + echo "[${ELAPSED}s] Stress running, ${MEM_FREE}MB free" +done + +# Restore original config +echo "" +echo "Restoring original CPU threshold (${ORIG_CYCLES} cycles)..." +sed -i "s/for 2 cycles/for ${ORIG_CYCLES} cycles/" /etc/monit/conf.d/cpu-killer +monit reload + +echo "" +echo "============================================" +echo "Tests Complete" +echo "============================================" diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..fd3cb4f --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -e + +# Runaway Process Killer - Uninstallation Script + +echo "============================================" +echo "Runaway Process Killer - Uninstallation" +echo "============================================" +echo "" + +# Check root +if [ "$EUID" -ne 0 ]; then + echo "Error: Please run as root (sudo ./uninstall.sh)" + exit 1 +fi + +read -p "This will remove earlyoom and monit. Continue? [y/N] " -n 1 -r +echo "" +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 0 +fi + +echo "[1/4] Stopping services..." +systemctl stop earlyoom monit 2>/dev/null || true + +echo "[2/4] Disabling services..." +systemctl disable earlyoom monit 2>/dev/null || true + +echo "[3/4] Removing packages..." +apt-get remove -y earlyoom monit + +echo "[4/4] Cleaning up config files..." +rm -f /usr/local/bin/kill-top-cpu.sh +rm -f /usr/local/bin/kill-orphan-claude.sh +rm -f /etc/monit/conf.d/cpu-killer +rm -f /etc/monit/conf.d/orphan-claude-killer + +echo "" +echo "============================================" +echo "Uninstallation Complete!" +echo "============================================" +echo "" +echo "Note: /etc/default/earlyoom and /etc/monit/monitrc may still exist." +echo "Remove manually if needed:" +echo " sudo rm -f /etc/default/earlyoom" +echo " sudo apt purge monit # removes /etc/monit/" +echo ""