#!/usr/bin/env bash # What memory this machine has, what is left, and — where there is one — what # cap is holding it there. # # Runs on native Linux and under WSL, because rig is developed on one and used # on the other. The difference is not cosmetic: on WSL the memory you see is a # VM allocation that can be raised, and the commonest failure is raising it # without restarting, so the number on disk and the number in /proc disagree. # On native Linux there is no such cap and pretending otherwise sends you to a # file that does not exist. # # This reports and instructs. It never writes a .wslconfig — applying one costs # a full VM restart that takes every shell, mount and container with it, and # choosing that moment is yours. # # `backup` exists so `restore` has something to read: back up, hand-edit # following the printed instruction, restore if it goes wrong. Both are # WSL-only, because .wslconfig is the only thing here worth backing up. # # Usage: mem.sh status | backup | restore set -euo pipefail # Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and # then fails in a pile of confusing ways: no /proc, no docker socket, none of # the tooling. Detectable, so name it instead. require_linux() { case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) cat >&2 <<'EOF' This has to run inside WSL, not Git Bash / MSYS / Cygwin. If WSL is not installed yet, from an elevated PowerShell or Command Prompt: wsl --install That enables Windows features and needs a reboot, so it is not something this script will do for you. Afterwards, open the Linux shell it installs and run this from there. See "Starting from plain Windows" in README.md. EOF exit 1 ;; esac } is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; } require_wsl() { if ! is_wsl; then echo "$1 acts on .wslconfig, which only exists under WSL." >&2 echo "This is native Linux — there is no VM allocation to save or roll back." >&2 echo "Use 'mem.sh status' to see what the machine actually has." >&2 exit 1 fi } mb() { echo $(( $(awk "/^$1:/{print \$2}" /proc/meminfo) / 1024 )); } # /mnt/c/Users can hold several real accounts — a renamed login leaves the old # directory behind — so picking the first alphabetically is a coin toss. Ask # Windows, then fall back to whichever profile actually owns a config. wslconfig_path() { local profile winpath found profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true) case "$profile" in ""|*%*) ;; *) winpath=$(wslpath -u "$profile" 2>/dev/null || true) if [ -n "$winpath" ] && [ -d "$winpath" ]; then echo "$winpath/.wslconfig"; return fi ;; esac found=$(ls -d /mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true) if [ -n "$found" ]; then echo "$found"; return; fi echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2 ls -d /mnt/c/Users/*/ 2>/dev/null \ | grep -viE "/(All Users|Default|Default User|Public)/$" | sed "s/^/ /" >&2 exit 1 } configured_memory() { [ -r "$1" ] || { echo ""; return; } sed -n 's/^[[:space:]]*memory[[:space:]]*=[[:space:]]*//p' "$1" | tail -1 | tr -d '[:space:]' } # "9GB" / "8192MB" / "9G" -> MB, so it can be compared with /proc/meminfo. to_mb() { local v="${1^^}" n n=$(echo "$v" | tr -dc '0-9') [ -n "$n" ] || { echo ""; return; } case "$v" in *GB|*G) echo $(( n * 1024 )) ;; *MB|*M) echo "$n" ;; *) echo $(( n / 1024 / 1024 )) ;; esac } hogs() { echo "holding the most:" ps -eo rss,comm --sort=-rss 2>/dev/null | awk 'NR>1 && NR<=6 {printf " %6.0f MB %s\n", $1/1024, $2}' } status() { local total avail swap_total swap_free total=$(mb MemTotal); avail=$(mb MemAvailable) swap_total=$(mb SwapTotal); swap_free=$(mb SwapFree) if is_wsl; then local cfg conf conf_mb cfg=$(wslconfig_path) conf=$(configured_memory "$cfg") echo "platform WSL" echo "config $cfg" if [ -n "$conf" ]; then conf_mb=$(to_mb "$conf") echo "configured $conf (${conf_mb} MB)" else conf_mb="" echo "configured (no memory= set — WSL defaults to 50% of host RAM, or 8GB, whichever is less)" fi echo "booted ${total} MB" echo "available ${avail} MB" echo "swap ${swap_total} MB ($(( swap_total - swap_free )) MB used)" if [ -n "$conf_mb" ]; then # The VM reports a little less than allocated; 15% covers the kernel # without calling every healthy machine a mismatch. if [ "$total" -lt $(( conf_mb * 85 / 100 )) ]; then echo echo "! configured ${conf_mb} MB but booted ${total} MB." echo " The change has not been applied. From a WINDOWS terminal:" echo echo " wsl --shutdown" echo echo " then start the distro again." fi else echo echo "To raise it, add to $cfg on the Windows side:" echo echo " [wsl2]" echo " memory=8GB" echo echo "then, from a WINDOWS terminal: wsl --shutdown" fi local n n=$(ls "$cfg".*.bak 2>/dev/null | wc -l) [ "$n" -gt 0 ] && echo "backups $n (newest: $(ls -t "$cfg".*.bak 2>/dev/null | head -1))" else echo "platform native linux" echo "total ${total} MB" echo "available ${avail} MB" echo "swap ${swap_total} MB ($(( swap_total - swap_free )) MB used)" echo echo "No VM allocation to raise here — this is the machine's own memory." echo "If it is tight the levers are freeing something or adding swap." fi # Under a fifth left is worth naming wherever you are running. if [ "$avail" -lt $(( total / 5 )) ]; then echo hogs fi return 0 } backup() { require_wsl backup local cfg dest cfg=$(wslconfig_path) [ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; } # Timestamped and never overwritten: a backup that can destroy itself on a # second run is not a backup. dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak" cp "$cfg" "$dest" echo "backed up $dest" echo echo "Edit $cfg by hand, then from a WINDOWS terminal: wsl --shutdown" } restore() { require_wsl restore local cfg newest count cfg=$(wslconfig_path) newest=$(ls -t "$cfg".*.bak 2>/dev/null | head -1 || true) [ -n "$newest" ] || { echo "no backups found beside $cfg" >&2; exit 1; } echo "restoring $newest" echo " -> $cfg" echo # Newest is the right default — undo the last edit — but if you backed up # *after* editing, the state you want is older. Show the rest so a no-op # restore is obviously a no-op rather than a mystery. count=$(ls "$cfg".*.bak 2>/dev/null | wc -l) if [ "$count" -gt 1 ]; then echo "$count backups exist, newest first:" ls -t "$cfg".*.bak | sed 's/^/ /' echo " (restoring the newest; copy another by hand to pick an older one)" echo fi if [ -r "$cfg" ]; then echo "what changes:" if diff "$cfg" "$newest" > /tmp/mem.diff 2>&1 && [ ! -s /tmp/mem.diff ]; then echo " nothing — that backup is identical to the current config" else sed 's/^/ /' /tmp/mem.diff fi rm -f /tmp/mem.diff echo fi printf "proceed? [y/N] " read -r reply case "$reply" in y|Y|yes|Yes) ;; *) echo "left alone"; return 0 ;; esac cp "$newest" "$cfg" echo "restored. From a WINDOWS terminal: wsl --shutdown" } require_linux case "${1:-status}" in status) status ;; backup) backup ;; restore) restore ;; *) echo "usage: $0 [status|backup|restore]" >&2; exit 1 ;; esac