207 lines
6.5 KiB
Bash
207 lines
6.5 KiB
Bash
|
|
#!/bin/bash
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
# Brainy Gate Preflight
|
||
|
|
# Refuses to let a test/build gate run on a machine that isn't clean enough
|
||
|
|
# to trust the numbers it produces. See scripts/gate/README.md for why (the
|
||
|
|
# 2026-08-13 lost-day ledger).
|
||
|
|
#
|
||
|
|
# Checks: 1-minute load average, any non-allowlisted process pinning a core,
|
||
|
|
# the cpu0 scaling governor, and free space on / and /tmp.
|
||
|
|
#
|
||
|
|
# Exit 0 and print one OK line per passing check when the machine is clean.
|
||
|
|
# Exit 1 and print one FATAL line per violation, naming the offender, when
|
||
|
|
# it is not.
|
||
|
|
#
|
||
|
|
# Known trap: a helper function whose last executed statement is a `while`
|
||
|
|
# (or any command whose own exit status happens to be nonzero) hands that
|
||
|
|
# status back as the function's return value. Called as a plain statement,
|
||
|
|
# that silently kills this script under `set -e`. Every helper below ends
|
||
|
|
# on an explicit `return 0` as its own statement, never on a loop or test.
|
||
|
|
#
|
||
|
|
# The same failure mode hides in plainer-looking lines too: `var=$(cmd)` is
|
||
|
|
# a bare assignment, so `set -e` DOES treat a nonzero `cmd` (or, under
|
||
|
|
# `pipefail`, a nonzero stage anywhere in `cmd`'s pipeline) as a failure of
|
||
|
|
# that statement and kills the script right there — even mid-loop, even
|
||
|
|
# when the "failure" is routine (a process that exited before a second
|
||
|
|
# lookup, a path that doesn't exist). Every such assignment below is paired
|
||
|
|
# with an explicit `|| var=""` fallback so a routine miss degrades to an
|
||
|
|
# empty value instead of an exit.
|
||
|
|
|
||
|
|
VIOLATIONS=0
|
||
|
|
ANCESTOR_PIDS=""
|
||
|
|
|
||
|
|
fatal() {
|
||
|
|
echo "FATAL: $1"
|
||
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
||
|
|
}
|
||
|
|
|
||
|
|
ok() {
|
||
|
|
echo "OK: $1"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Walks this process's parent chain up to pid 1, then takes one snapshot of
|
||
|
|
# its direct children (the ps/read pipeline in check_processes), and
|
||
|
|
# records both in ANCESTOR_PIDS — so the process-scan below can recognize
|
||
|
|
# its own tree (the shell/terminal/session that launched it, plus its own
|
||
|
|
# helper commands) instead of flagging it. Children are captured once, up
|
||
|
|
# front, rather than re-queried per row later, so a helper command that has
|
||
|
|
# already exited by the time it's looked up can't be mistaken for a miss.
|
||
|
|
build_ancestor_pids() {
|
||
|
|
local pid="$$"
|
||
|
|
local ppid child
|
||
|
|
ANCESTOR_PIDS=" $pid "
|
||
|
|
while [ "$pid" != "1" ]; do
|
||
|
|
ppid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ') || ppid=""
|
||
|
|
if [ -z "$ppid" ]; then
|
||
|
|
break
|
||
|
|
fi
|
||
|
|
ANCESTOR_PIDS="${ANCESTOR_PIDS}${ppid} "
|
||
|
|
pid="$ppid"
|
||
|
|
done
|
||
|
|
|
||
|
|
while IFS= read -r child; do
|
||
|
|
[ -z "$child" ] && continue
|
||
|
|
ANCESTOR_PIDS="${ANCESTOR_PIDS}${child} "
|
||
|
|
done < <(ps --ppid "$$" -o pid= 2>/dev/null || true)
|
||
|
|
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
|
||
|
|
# (a) 1-minute load average vs. threshold (default: nproc / 2).
|
||
|
|
check_load() {
|
||
|
|
local max_load="${GATE_MAX_LOAD:-}"
|
||
|
|
if [ -z "$max_load" ]; then
|
||
|
|
max_load=$(( $(nproc) / 2 ))
|
||
|
|
if [ "$max_load" -lt 1 ]; then
|
||
|
|
max_load=1
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
local load_1m
|
||
|
|
load_1m=$(cut -d' ' -f1 /proc/loadavg)
|
||
|
|
|
||
|
|
if awk -v l="$load_1m" -v m="$max_load" 'BEGIN { exit !(l > m) }'; then
|
||
|
|
fatal "1-minute load average ${load_1m} exceeds threshold ${max_load} (GATE_MAX_LOAD=${max_load})"
|
||
|
|
else
|
||
|
|
ok "1-minute load average ${load_1m} is within threshold ${max_load}"
|
||
|
|
fi
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
|
||
|
|
# (b) any process outside the allowlist pinning more than half a core.
|
||
|
|
# Parsed with `read` into named fields, not an awk/cut chain — a fixed-column
|
||
|
|
# awk/cut split on `ps` output duplicated fields the first time this was
|
||
|
|
# tried, because process args vary in word count. `read` with a fixed list
|
||
|
|
# of variables dumps everything left over into the last one (args), which
|
||
|
|
# handles that correctly.
|
||
|
|
check_processes() {
|
||
|
|
local max_pcpu=50
|
||
|
|
local extra_regex="${GATE_ALLOW_REGEX:-}"
|
||
|
|
local violation_found=0
|
||
|
|
local line pcpu pid args pcpu_int
|
||
|
|
|
||
|
|
while IFS= read -r line; do
|
||
|
|
[ -z "$line" ] && continue
|
||
|
|
read -r pcpu pid args <<< "$line"
|
||
|
|
|
||
|
|
# Kernel threads report their comm in brackets, e.g. "[kworker/0:1]".
|
||
|
|
case "$args" in
|
||
|
|
\[*\]) continue ;;
|
||
|
|
esac
|
||
|
|
|
||
|
|
# This script's own tree: its ancestors (shell, terminal, session) and
|
||
|
|
# its direct children, both captured once by build_ancestor_pids.
|
||
|
|
case " $ANCESTOR_PIDS " in
|
||
|
|
*" $pid "*) continue ;;
|
||
|
|
esac
|
||
|
|
|
||
|
|
case "$args" in
|
||
|
|
*sshd*|*systemd*) continue ;;
|
||
|
|
esac
|
||
|
|
|
||
|
|
if [ -n "$extra_regex" ] && [[ "$args" =~ $extra_regex ]]; then
|
||
|
|
continue
|
||
|
|
fi
|
||
|
|
|
||
|
|
pcpu_int="${pcpu%.*}"
|
||
|
|
if [ -z "$pcpu_int" ]; then
|
||
|
|
pcpu_int=0
|
||
|
|
fi
|
||
|
|
if [ "$pcpu_int" -gt "$max_pcpu" ]; then
|
||
|
|
fatal "pid ${pid} ('${args}') is using ${pcpu}% of one core"
|
||
|
|
violation_found=1
|
||
|
|
fi
|
||
|
|
done < <(ps -eo pcpu,pid,args --sort=-pcpu | tail -n +2)
|
||
|
|
|
||
|
|
if [ "$violation_found" -eq 0 ]; then
|
||
|
|
ok "no process outside the allowlist exceeds ${max_pcpu}% of one core"
|
||
|
|
fi
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
|
||
|
|
# (c) cpu0 scaling governor must be "performance". Skipped with a warning
|
||
|
|
# (not a violation) when the sysfs path doesn't exist on this machine.
|
||
|
|
check_governor() {
|
||
|
|
local gov_path="/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"
|
||
|
|
if [ ! -r "$gov_path" ]; then
|
||
|
|
echo "WARNING: ${gov_path} not present; skipping governor check"
|
||
|
|
return 0
|
||
|
|
fi
|
||
|
|
|
||
|
|
local governor
|
||
|
|
governor=$(cat "$gov_path" 2>/dev/null) || governor=""
|
||
|
|
if [ "$governor" != "performance" ]; then
|
||
|
|
fatal "cpu0 governor is '${governor}', not 'performance'"
|
||
|
|
else
|
||
|
|
ok "cpu0 governor is 'performance'"
|
||
|
|
fi
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
|
||
|
|
# (d) free-space floors on / and /tmp (default 10G each). Skip entirely via
|
||
|
|
# GATE_SKIP_DISK_CHECK=1.
|
||
|
|
check_disk() {
|
||
|
|
if [ "${GATE_SKIP_DISK_CHECK:-0}" = "1" ]; then
|
||
|
|
echo "WARNING: disk free-space check skipped (GATE_SKIP_DISK_CHECK=1)"
|
||
|
|
return 0
|
||
|
|
fi
|
||
|
|
|
||
|
|
local floor_gb=10
|
||
|
|
local floor_bytes=$((floor_gb * 1024 * 1024 * 1024))
|
||
|
|
local path avail_bytes avail_gb
|
||
|
|
|
||
|
|
for path in / /tmp; do
|
||
|
|
avail_bytes=$(df --output=avail -B1 "$path" 2>/dev/null | tail -n 1 | tr -d ' ') || avail_bytes=""
|
||
|
|
if [ -z "$avail_bytes" ]; then
|
||
|
|
echo "WARNING: could not determine free space on ${path}; skipping"
|
||
|
|
continue
|
||
|
|
fi
|
||
|
|
if [ "$avail_bytes" -lt "$floor_bytes" ]; then
|
||
|
|
avail_gb=$((avail_bytes / 1024 / 1024 / 1024))
|
||
|
|
fatal "${path} has only ${avail_gb}G free, below the ${floor_gb}G floor"
|
||
|
|
else
|
||
|
|
ok "${path} has enough free space (floor ${floor_gb}G)"
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
|
||
|
|
echo "Brainy gate preflight"
|
||
|
|
echo "----------------------"
|
||
|
|
|
||
|
|
build_ancestor_pids
|
||
|
|
check_load
|
||
|
|
check_processes
|
||
|
|
check_governor
|
||
|
|
check_disk
|
||
|
|
|
||
|
|
echo "----------------------"
|
||
|
|
if [ "$VIOLATIONS" -gt 0 ]; then
|
||
|
|
echo "FATAL: gate preflight failed with ${VIOLATIONS} violation(s) — machine is not gate-clean"
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo "gate preflight passed — machine is gate-clean"
|
||
|
|
exit 0
|