← back to Cncp Mcp

scripts/audit-pm2-caps.sh

69 lines

#!/usr/bin/env bash
# audit-pm2-caps.sh — read-only audit of pm2 procs missing memory/CPU caps.
#
# Soft-caps are Steve's protection against a single leaking pm2 proc dragging
# load to 500+. Each process SHOULD have `max_memory_restart` set in its
# ecosystem.config.{js,cjs}. This script finds the gaps and prints a punch list.
#
# Usage:
#   bash ~/Projects/cncp-mcp/scripts/audit-pm2-caps.sh
#
# Output: one line per cap-less proc with the candidate ecosystem file path.

set -u

PM2_BIN="${PM2_BIN:-$HOME/.npm-global/bin/pm2}"
[[ -x "$PM2_BIN" ]] || PM2_BIN="$(/usr/bin/which pm2 2>/dev/null || echo pm2)"

"$PM2_BIN" jlist 2>/dev/null > /tmp/audit-pm2-caps.json
/usr/bin/python3 <<'PY'
import json, os
try:
    procs = json.load(open('/tmp/audit-pm2-caps.json'))
except Exception as e:
    print(f"pm2 jlist parse failed: {e}")
    raise SystemExit(1)

uncapped = []
capped = []
for p in procs:
    env = p.get('pm2_env', {})
    name = p.get('name', '?')
    max_mem = env.get('max_memory_restart')
    pm_exec_path = env.get('pm_exec_path', '')
    pm_cwd = env.get('pm_cwd', '')
    # Find candidate ecosystem file
    eco = None
    for candidate in ('ecosystem.config.js', 'ecosystem.config.cjs'):
        path = os.path.join(pm_cwd, candidate) if pm_cwd else ''
        if path and os.path.isfile(path):
            eco = path
            break
    row = (name, max_mem, eco or '(no ecosystem.config.{js,cjs} found)')
    if max_mem:
        capped.append(row)
    else:
        uncapped.append(row)

print(f"=== capped procs: {len(capped)} ===")
for n, m, e in capped[:10]:
    print(f"  ✓ {n:<32} max_memory_restart={m}")
if len(capped) > 10:
    print(f"  ... and {len(capped)-10} more")

print()
print(f"=== UNCAPPED procs (no max_memory_restart): {len(uncapped)} ===")
for n, m, e in uncapped:
    print(f"  ✗ {n:<32} → {e}")

print()
print("Fix pattern in each ecosystem.config.{js,cjs}:")
print("  module.exports = { apps: [{ name: '...', script: '...',")
print("    max_memory_restart: '500M',   // sister-sites: try 300M; LLM agents: 1G")
print("    cron_restart: '0 4 * * *',    // optional nightly bounce at 4am")
print("  }] };")
print()
print("After editing each ecosystem file:")
print("  pm2 reload <name> --update-env")
PY