diff --git a/scripts/setup/machine-setup/lib/system.sh b/scripts/setup/machine-setup/lib/system.sh index dd2654ba..ffc261d8 100644 --- a/scripts/setup/machine-setup/lib/system.sh +++ b/scripts/setup/machine-setup/lib/system.sh @@ -173,6 +173,168 @@ swappiness_set() { sysctl -q -w "vm.swappiness=$1" } +# ----------------------------------------------------------------------------- +# Emergency disk ballast +# ----------------------------------------------------------------------------- +# +# The same idea as swap, one layer down. Swap is the valve for memory pressure; +# this is the valve for disk pressure. +# +# A junk file holding no data, sized at 10% of free disk. Its only job is to be +# deleted when the filesystem is about to fill, buying enough headroom to log in +# and clean up properly instead of meeting a wedged box — Docker, journald and +# postgres all misbehave badly at 100% full, and some of them do not recover on +# their own. +# +# A one-shot valve: once spent, it has to be recreated. +# +# ── Moved out of the user's home ── +# +# The original put the checker in $USER_HOME/.local/bin and ran it from a root +# cron. A root cron executing a script inside a directory its owner can write is +# a privilege escalation waiting to be noticed — moot on a box where that user +# already has passwordless sudo, but wrong, and not something to carry forward. +# Both the script and the file now live in root-owned system paths. + +BALLAST_FILE=/var/lib/machine-setup/ballast.bin +BALLAST_CHECKER=/usr/local/sbin/emergency-disk-check +BALLAST_CRON=/etc/cron.d/emergency-disk-check +BALLAST_THRESHOLD=10 + +ballast_exists() { [[ -f "$BALLAST_FILE" ]]; } + +ballast_size_human() { du -h "$BALLAST_FILE" 2>/dev/null | cut -f1; } + +# 10% of what is free right now, in MiB. +ballast_size_mb() { + local free_kb + free_kb="$(df -Pk /var/lib | awk 'NR == 2 { print $4 }')" + echo $((free_kb / 1024 / 10)) +} + +ballast_create() { + local mb="$1" + mkdir -p "$(dirname "$BALLAST_FILE")" + + # fallocate reserves real blocks. A sparse file made with truncate would + # reserve nothing and free nothing when deleted, which is the entire point. + if ! fallocate -l "${mb}M" "$BALLAST_FILE" 2>/dev/null; then + info " fallocate is not usable here — writing with dd, which is slower" + dd if=/dev/zero of="$BALLAST_FILE" bs=1M count="$mb" status=none + fi + chmod 600 "$BALLAST_FILE" +} + +ballast_install_checker() { + mkdir -p "$(dirname "$BALLAST_FILE")" + cat >"$BALLAST_CHECKER" <"$BALLAST_CRON" </dev/null; } + +earlyoom_install() { + pkg_is_installed earlyoom || pkg_install_now earlyoom + systemctl enable --now earlyoom >/dev/null 2>&1 +} + +# ----------------------------------------------------------------------------- +# Resource limits +# ----------------------------------------------------------------------------- +# +# inotify watches: how many files one user can have the kernel watching. The +# stock limit is small enough that one file watcher walking +# node_modules. Every watcher on the machine draws from the same pool. +# +# The failure is silent, which is what makes it worth setting in advance: nothing +# errors, the watcher simply stops noticing changes. Hot reload goes quiet, a +# build stops rebuilding, and the reason is never on screen. +# +# Mostly a development concern, but not exclusively — anything running `bun +# --watch` or serving a file browser is a watcher too. + +INOTIFY_WATCHES=524288 +INOTIFY_INSTANCES=1024 + +inotify_current_watches() { sysctl -n fs.inotify.max_user_watches 2>/dev/null || echo 0; } + +inotify_raise() { + cat >/etc/sysctl.d/99-machine-setup-inotify.conf <= INOTIFY_WATCHES)); then + echo " already at or above that, nothing to do" + SUMMARY+=("inotify watches: already ${CURRENT_WATCHES}") + elif confirm "Raise it?"; then + inotify_raise + ok "inotify watches raised to $(inotify_current_watches)" + SUMMARY+=("inotify watches: raised to ${INOTIFY_WATCHES}") + else + warn "skipped by request" + SUMMARY+=("inotify watches: SKIPPED by request — left at ${CURRENT_WATCHES}") + fi + step_ok +fi + # ============================================================================= # NOT PORTED YET # ============================================================================= @@ -324,7 +441,7 @@ fi # auto-suspend · boot-hang fix · user creation · # ssh keys · ssh hardening · dns · static ip · fail2ban · unattended-upgrades · # git config · docker · zsh + prompt · tailscale · neovim · js runtimes · -# dev tools · ufw · zshrc · disk ballast +# dev tools · ufw · zshrc # # Each arrives as its own commit. Delete this block when the list is empty.