Two of Section F. Hooks are the pressure valve. "Can Panama also do X when the theme changes" is now a five-line file in ~/.config/panama/hooks rather than a fork, a feature request, or a patch somebody rebases forever. Each name takes a single file and a .d directory so several things can react without fighting over one, and a broken hook is reported and stepped over: somebody's script must never cost a theme change, an upgrade or a login. Wired at theme-set, post-upgrade and post-migrate. This is the thirty-line version of the plugin host the upstream ledger defers, and it has no API to keep stable beyond "we will run your script and tell you what happened". Testing it caught a real bug the reading would not have: run_one captured the script path but never shifted it off, so every hook got its own filename as $1 and the real arguments arrived one place late. A hook reading $1 as the colour scheme got a path. The crash watcher notices when a program dumps core and says so. Under GNOME, ABRT does this; here nothing did, and applications died silently, which is most of how "Linux is flaky" gets earned. Once per program per session is the entire design, not a nicety. This machine's portal backend crashes between eleven and sixty times a day, and a notification per crash would be one every few minutes for something nobody can act on. The first is news; the fortieth is why people turn notifications off. The health page keeps the running count. It waits for the notification server before reporting, because the crash most worth hearing about is the one that took the shell with it, and it names the executable rather than the kernel's comm field, which truncates at fifteen characters. Verified against real segfaults.
189 lines
6.8 KiB
Bash
Executable File
189 lines
6.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Bringing an already-installed machine up to what this checkout expects.
|
|
#
|
|
# `./install` is additive: it copies files over `/`, links dotfiles, and
|
|
# installs packages. It has no way to say "remove that file", "disable that
|
|
# unit", "that symlink now points at the wrong place". So a machine installed
|
|
# in March keeps March's mistakes forever, and the only thing that ever fixes
|
|
# them is the person who happens to read a commit message.
|
|
#
|
|
# A migration is one shell script that performs one such repair, exactly once,
|
|
# on machines that need it.
|
|
#
|
|
# migrations/<unix-timestamp>.sh
|
|
#
|
|
# The name is the commit timestamp of HEAD when it was authored, so glob order
|
|
# over fixed-width epoch seconds IS chronological order -- no sequence numbers
|
|
# to collide on across branches. `panama-dev-migration` stamps them.
|
|
#
|
|
# State is one empty marker file per migration under
|
|
# $XDG_STATE_HOME/panama/migrations. Present means applied. There is no
|
|
# database and no version integer, because the failure mode of a version
|
|
# integer is that one bad migration strands every later one behind it.
|
|
#
|
|
# The rules a migration must follow are in the template that
|
|
# `panama-dev-migration` writes, and they are worth repeating here because
|
|
# this runner cannot enforce them:
|
|
#
|
|
# * Safe to run twice. The marker only records that it succeeded once.
|
|
# * Tolerant of the repair already being correct -- a user may have fixed it
|
|
# by hand, or a later `./install` may have overwritten it back.
|
|
# * Root work goes through `panama-sudo --reason "..."`, never bare sudo,
|
|
# so the prompt names the repair. See bin/panama-sudo.
|
|
#
|
|
# The marker is written ONLY on success, so a migration that fails stays
|
|
# pending and is retried at the next login. That is deliberate: a repair that
|
|
# could not complete has not happened, and recording it as done would hide it
|
|
# forever.
|
|
#
|
|
# This mirrors config/dot/quickshell/config/Migrations.qml, which does the same
|
|
# job for the settings JSON and documents the same reasoning. That one handles
|
|
# renamed preference keys; this one handles everything else.
|
|
|
|
set -uo pipefail
|
|
|
|
PANAMA_PATH="${PANAMA_PATH:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)}"
|
|
MIGRATIONS_DIR="$PANAMA_PATH/migrations"
|
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama/migrations"
|
|
|
|
export PANAMA_PATH
|
|
|
|
info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
|
ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; }
|
|
warn() { printf '\033[1;33m!\033[0m %s\n' "$*" >&2; }
|
|
err() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; }
|
|
|
|
# Every migration this checkout ships, oldest first. Empty is a valid state.
|
|
all_migrations() {
|
|
[[ -d "$MIGRATIONS_DIR" ]] || return 0
|
|
local file
|
|
for file in "$MIGRATIONS_DIR"/*.sh; do
|
|
[[ -e "$file" ]] || continue
|
|
basename "$file"
|
|
done | sort
|
|
}
|
|
|
|
pending_migrations() {
|
|
local name
|
|
while read -r name; do
|
|
[[ -n "$name" ]] || continue
|
|
[[ -e "$STATE_DIR/$name" ]] || printf '%s\n' "$name"
|
|
done < <(all_migrations)
|
|
}
|
|
|
|
run_one() {
|
|
local name="$1" file="$MIGRATIONS_DIR/$1"
|
|
info "$name"
|
|
# A subshell with its own strictness: a migration that forgets `set -e` is
|
|
# still stopped by its first failing command, and one that sets shell
|
|
# options cannot leak them into the next migration.
|
|
if bash -euo pipefail "$file"; then
|
|
mkdir -p "$STATE_DIR"
|
|
: >"$STATE_DIR/$name"
|
|
ok "$name applied"
|
|
return 0
|
|
fi
|
|
err "$name failed and will be retried at the next login"
|
|
return 1
|
|
}
|
|
|
|
cmd_run() {
|
|
local pending
|
|
pending="$(pending_migrations)"
|
|
if [[ -z "$pending" ]]; then
|
|
ok "Nothing to migrate; this machine matches the checkout."
|
|
return 0
|
|
fi
|
|
|
|
local count failed=0 name
|
|
count="$(grep -c . <<<"$pending")"
|
|
info "$count migration(s) to apply"
|
|
while read -r name; do
|
|
[[ -n "$name" ]] || continue
|
|
# Stop at the first failure rather than continuing. Migrations are
|
|
# ordered, and a later one may assume an earlier one landed; running
|
|
# it anyway turns one stuck repair into an unpredictable machine.
|
|
if ! run_one "$name"; then
|
|
failed=1
|
|
break
|
|
fi
|
|
done <<<"$pending"
|
|
|
|
if (( failed )); then
|
|
warn "Re-running 'panama migrate' is safe and will retry from the failure."
|
|
return 1
|
|
fi
|
|
ok "This machine now matches the checkout."
|
|
# Only after repairs actually ran: a hook that fires on every login when
|
|
# there was nothing to do is a hook people disable.
|
|
"$PANAMA_PATH/bin/panama-hook" post-migrate || true
|
|
}
|
|
|
|
# The check the login notifier runs. Exit 0 means work is waiting, so it reads
|
|
# as `if panama-migrate --pending; then notify; fi`.
|
|
cmd_pending() {
|
|
local pending
|
|
pending="$(pending_migrations)"
|
|
[[ -n "$pending" ]] || return 1
|
|
grep -c . <<<"$pending"
|
|
}
|
|
|
|
cmd_list() {
|
|
local name
|
|
while read -r name; do
|
|
[[ -n "$name" ]] || continue
|
|
if [[ -e "$STATE_DIR/$name" ]]; then
|
|
printf 'applied %s\n' "$name"
|
|
else
|
|
printf 'pending %s\n' "$name"
|
|
fi
|
|
done < <(all_migrations)
|
|
}
|
|
|
|
# Re-run one that already succeeded. For developing a migration, and for the
|
|
# rare case where a repair was undone by something else.
|
|
cmd_force() {
|
|
local name="${1:-}"
|
|
[[ -n "$name" ]] || { err "force needs a migration name"; return 2; }
|
|
[[ -e "$MIGRATIONS_DIR/$name" ]] || { err "no such migration: $name"; return 2; }
|
|
rm -f "$STATE_DIR/$name"
|
|
run_one "$name"
|
|
}
|
|
|
|
# Mark everything applied without running it. This is what a fresh install
|
|
# does: the machine was just built from this checkout, so every repair those
|
|
# migrations describe is already true of it, and running them would apply
|
|
# fixes for versions it never had.
|
|
cmd_baseline() {
|
|
mkdir -p "$STATE_DIR"
|
|
local name count=0
|
|
while read -r name; do
|
|
[[ -n "$name" ]] || continue
|
|
[[ -e "$STATE_DIR/$name" ]] && continue
|
|
: >"$STATE_DIR/$name"
|
|
count=$(( count + 1 ))
|
|
done < <(all_migrations)
|
|
ok "Marked $count migration(s) as already applied."
|
|
}
|
|
|
|
case "${1:-run}" in
|
|
run) shift || true; cmd_run "$@" ;;
|
|
--pending) shift || true; cmd_pending "$@" ;;
|
|
--list|list) shift || true; cmd_list "$@" ;;
|
|
--force) shift || true; cmd_force "$@" ;;
|
|
--baseline) shift || true; cmd_baseline "$@" ;;
|
|
-h|--help)
|
|
cat <<'USAGE'
|
|
usage: panama-migrate [run|--pending|--list|--force <name>|--baseline]
|
|
|
|
run Apply every pending migration, oldest first (default)
|
|
--pending Exit 0 and print the count when work is waiting, else exit 1
|
|
--list Show every migration and whether it has been applied
|
|
--force Re-run one migration that already succeeded
|
|
--baseline Mark everything applied without running it (fresh installs)
|
|
USAGE
|
|
;;
|
|
*) err "unknown argument: $1"; exit 2 ;;
|
|
esac
|