234 lines
7.9 KiB
Bash
Executable File
234 lines
7.9 KiB
Bash
Executable File
#!/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
|