Let somebody extend this without forking it, and say when things die
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.
This commit is contained in:
Executable
+145
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# The pressure valve.
|
||||
#
|
||||
# Hooks exist so "can Panama also do X when the theme changes" is a five-line
|
||||
# file somebody drops in a directory rather than a fork, a feature request, or
|
||||
# a patch that has to be rebased forever. The upstream ledger defers a plugin
|
||||
# host as premature and still should; this covers most of what people actually
|
||||
# want from one and has no API to keep stable beyond "we will run your script
|
||||
# and tell you what happened".
|
||||
#
|
||||
# What must hold:
|
||||
#
|
||||
# 1. A failing hook is reported and stepped over. Somebody's broken script
|
||||
# must never break a theme change, an upgrade, or a login -- and the
|
||||
# hooks after it still run.
|
||||
# 2. Arguments arrive as written. The first version passed each hook its own
|
||||
# path as $1, so every argument landed one place late; a hook reading $1
|
||||
# as the colour scheme got a filename.
|
||||
# 3. Both the single file and the .d directory run, .d in sorted order, so
|
||||
# several things can react without fighting over one file.
|
||||
# 4. A hook name cannot escape the hook directory.
|
||||
# 5. No hooks at all is the normal case and exits cleanly.
|
||||
# 6. Samples are copied, never symlinked. ~/.config/panama is the user's --
|
||||
# settings.json lives there -- and a symlinked directory would put their
|
||||
# scripts in the repository working tree.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
hook="$repo_dir/bin/panama-hook"
|
||||
samples="$repo_dir/config/dot/panama/hooks"
|
||||
linker="$repo_dir/setup/scripts/link-dotfiles"
|
||||
|
||||
findings=()
|
||||
note() { findings+=("$1"); }
|
||||
|
||||
[[ -x "$hook" ]] || { printf 'hooks contract: %s is not executable\n' "$hook" >&2; exit 1; }
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
hooks="$work/hooks"
|
||||
out="$work/out"
|
||||
mkdir -p "$hooks"
|
||||
|
||||
run() { PANAMA_HOOK_DIR="$hooks" "$hook" "$@" 2>"$work/err"; }
|
||||
|
||||
# ── 5. Nothing to run ───────────────────────────────────────────────────────
|
||||
|
||||
run theme-set dark blue || note 'a hook name with no hooks behind it did not exit cleanly'
|
||||
|
||||
# ── 2 & 3. Arguments, and both places hooks live ────────────────────────────
|
||||
|
||||
: >"$out"
|
||||
cat >"$hooks/theme-set" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
printf 'single:%s,%s\n' "\$1" "\$2" >>"$out"
|
||||
EOF
|
||||
mkdir -p "$hooks/theme-set.d"
|
||||
cat >"$hooks/theme-set.d/20-second" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
printf 'second:%s,%s\n' "\$1" "\$2" >>"$out"
|
||||
EOF
|
||||
cat >"$hooks/theme-set.d/10-first" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
printf 'first:%s,%s\n' "\$1" "\$2" >>"$out"
|
||||
EOF
|
||||
chmod +x "$hooks/theme-set" "$hooks/theme-set.d"/*
|
||||
|
||||
run theme-set dark orchid || note 'running hooks reported failure when none failed'
|
||||
|
||||
grep -qx 'single:dark,orchid' "$out" \
|
||||
|| note "the single hook did not receive its arguments as written (got: $(grep '^single' "$out" || echo none))"
|
||||
grep -qx 'first:dark,orchid' "$out" \
|
||||
|| note 'a hook in the .d directory did not receive its arguments as written'
|
||||
[[ "$(grep -c . "$out")" == "3" ]] \
|
||||
|| note "expected three hooks to run, got $(grep -c . "$out")"
|
||||
[[ "$(sed -n '2p' "$out")" == first:* && "$(sed -n '3p' "$out")" == second:* ]] \
|
||||
|| note 'the .d hooks did not run in sorted order'
|
||||
|
||||
# A file that is not executable is not a hook. Dropping a note in the
|
||||
# directory should not be an error.
|
||||
printf 'not a script\n' >"$hooks/theme-set.d/30-readme"
|
||||
: >"$out"
|
||||
run theme-set dark orchid || note 'a non-executable file in the .d directory caused a failure'
|
||||
[[ "$(grep -c . "$out")" == "3" ]] \
|
||||
|| note 'a non-executable file was treated as a hook'
|
||||
rm -f "$hooks/theme-set.d/30-readme"
|
||||
|
||||
# ── 1. A broken hook is stepped over ────────────────────────────────────────
|
||||
|
||||
cat >"$hooks/theme-set.d/05-broken" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
exit 7
|
||||
EOF
|
||||
chmod +x "$hooks/theme-set.d/05-broken"
|
||||
|
||||
: >"$out"
|
||||
run theme-set dark orchid \
|
||||
|| note 'a failing hook made the whole run fail, so a broken script would break a theme change'
|
||||
grep -q 'failed' "$work/err" \
|
||||
|| note 'a failing hook was silent, so nobody would know their script is broken'
|
||||
[[ "$(grep -c . "$out")" == "3" ]] \
|
||||
|| note 'a failing hook stopped the hooks after it from running'
|
||||
|
||||
# ── 4. A hook name cannot escape ────────────────────────────────────────────
|
||||
|
||||
mkdir -p "$work/outside"
|
||||
printf '#!/usr/bin/env bash\ntouch "%s/escaped"\n' "$work/outside" >"$work/outside/evil"
|
||||
chmod +x "$work/outside/evil"
|
||||
run ../outside/evil >/dev/null 2>&1 \
|
||||
&& note 'a hook name containing a path separator was accepted'
|
||||
[[ -e "$work/outside/escaped" ]] \
|
||||
&& note 'a hook outside the hook directory was executed'
|
||||
|
||||
# ── 6. Samples ship, and are copied rather than symlinked ───────────────────
|
||||
|
||||
[[ -d "$samples" ]] || note 'no hook samples are shipped, so the mechanism is undiscoverable'
|
||||
for sample in "$samples"/*.sample; do
|
||||
[[ -e "$sample" ]] || continue
|
||||
head -1 "$sample" | grep -q '^#!' \
|
||||
|| note "$(basename "$sample") has no shebang, so copying it and adding +x would not run"
|
||||
done
|
||||
grep -q 'PANAMA_HOOK_SAMPLES' "$linker" \
|
||||
|| note 'link-dotfiles does not install the hook samples'
|
||||
grep -A12 'PANAMA_HOOK_SAMPLES' "$linker" | grep -q 'cp "\$sample"' \
|
||||
|| note 'the hook samples are not copied; a symlinked hook directory would put user scripts in the repository'
|
||||
|
||||
# ── The call sites ──────────────────────────────────────────────────────────
|
||||
|
||||
for pair in "config/dot/quickshell/scripts/panama-theme-apps:theme-set" \
|
||||
"bin/panama-migrate:post-migrate" \
|
||||
"install:post-upgrade"; do
|
||||
file="${pair%%:*}"; name="${pair##*:}"
|
||||
grep -q "panama-hook\" $name\|panama-hook\" \"$name\"\|hook\" $name" "$repo_dir/$file" \
|
||||
|| note "$file does not fire the $name hook"
|
||||
done
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
printf 'hooks contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'hooks contract: PASS\n'
|
||||
Reference in New Issue
Block a user