144 lines
6.6 KiB
Bash
Executable File
144 lines
6.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Tell somebody when a program crashes.
|
|
#
|
|
# On GNOME, ABRT says so. Under a hand-assembled Hyprland desktop nothing does,
|
|
# and applications die silently -- which is most of how "Linux is flaky" gets
|
|
# earned. Fedora ships systemd-coredump by default, so the information is
|
|
# already there; nobody is reading it.
|
|
#
|
|
# Follows the journal for systemd-coredump's own message id and reports each
|
|
# program once per session.
|
|
#
|
|
# ONCE PER SESSION IS THE WHOLE DESIGN. This machine's portal backend crashes
|
|
# between eleven and sixty times a day -- see the portal-stability check in
|
|
# panama-doctor -- and a notification per crash would be a notification every
|
|
# few minutes for something the user can do nothing about. The first one is
|
|
# news; the fortieth is why people turn notifications off. The health page
|
|
# carries the running count for anyone who wants it.
|
|
#
|
|
# When an agent has been chosen, the notification stops being a dead end. It
|
|
# carries the diagnosis command as data in a `panama-exec` hint, which the shell
|
|
# runs on click. Command-as-data rather than a libnotify action, because an
|
|
# action would tie the click to this process still being alive to hear it, and
|
|
# this process is a `journalctl -f` that outlives nothing in particular. The
|
|
# hint survives a shell restart and never blocks the watcher.
|
|
|
|
set -uo pipefail
|
|
|
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
|
SETTINGS="${PANAMA_AGENT_SETTINGS:-${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json}"
|
|
|
|
# systemd-coredump's MESSAGE_ID. Matching on this rather than on text keeps
|
|
# working when the wording changes and never matches a program that merely
|
|
# mentions the word "crash" in its own logs.
|
|
readonly COREDUMP_MESSAGE_ID='fc2e22bc6ee647b6b90729ab34a250b1'
|
|
|
|
command -v journalctl >/dev/null 2>&1 || exit 0
|
|
command -v notify-send >/dev/null 2>&1 || exit 0
|
|
|
|
# The shell owns org.freedesktop.Notifications, and the crash most worth
|
|
# reporting is the one that took the shell with it. Waiting means that report
|
|
# arrives rather than vanishing into a bus nobody is serving.
|
|
for _ in $(seq 1 60); do
|
|
busctl --user status org.freedesktop.Notifications >/dev/null 2>&1 && break
|
|
sleep 1
|
|
done
|
|
|
|
# Read per crash rather than once at startup, so choosing an agent in Settings
|
|
# takes effect on the next crash instead of on the next login. This service runs
|
|
# for the life of the session; nothing restarts it when a preference changes.
|
|
read_setting() {
|
|
local key="$1" fallback="$2"
|
|
[[ -r "$SETTINGS" ]] || { printf '%s' "$fallback"; return; }
|
|
command -v jq >/dev/null 2>&1 || { printf '%s' "$fallback"; return; }
|
|
jq -r --arg k "$key" --arg d "$fallback" \
|
|
'if has($k) and (.[$k] != null) then (.[$k] | tostring) else $d end' \
|
|
"$SETTINGS" 2>/dev/null || printf '%s' "$fallback"
|
|
}
|
|
|
|
# What to call the agent in a sentence aimed at a person.
|
|
agent_label() {
|
|
case "$1" in
|
|
claude) printf 'Claude Code' ;;
|
|
codex) printf 'Codex' ;;
|
|
*) printf '%s' "$1" ;;
|
|
esac
|
|
}
|
|
|
|
declare -A reported=()
|
|
|
|
# -f from now, not from the boot: a session that starts after a crash should
|
|
# not open with a notification about something the user has already lived
|
|
# through and cannot act on.
|
|
journalctl --user -f -n 0 --output=json MESSAGE_ID="$COREDUMP_MESSAGE_ID" 2>/dev/null \
|
|
| while IFS= read -r line; do
|
|
[[ -n "$line" ]] || continue
|
|
|
|
# One jq per entry rather than one per field: the fields are read
|
|
# together, and the click payload needs all of them.
|
|
IFS=$'\t' read -r uid exe comm pid signal < <(
|
|
jq -r '[(.COREDUMP_UID // ""),
|
|
(.COREDUMP_EXE // ""),
|
|
(.COREDUMP_COMM // ""),
|
|
(.COREDUMP_PID // ""),
|
|
(.COREDUMP_SIGNAL_NAME // "")] | @tsv' <<<"$line" 2>/dev/null
|
|
)
|
|
|
|
# Another user's crash is not this session's business, and reporting it
|
|
# would leak what they are running.
|
|
[[ "$uid" == "$(id -u)" ]] || continue
|
|
[[ -n "$exe" || -n "$comm" ]] || continue
|
|
|
|
# The executable name first: COREDUMP_COMM is the kernel's comm field
|
|
# and is truncated to fifteen characters, so it reports
|
|
# "panama-test-cra" for a program called panama-test-crasher.
|
|
if [[ -n "$exe" ]]; then
|
|
program="$(basename "$exe")"
|
|
else
|
|
program="$comm"
|
|
fi
|
|
|
|
# Never announce our own machinery. A crash watcher that notifies about
|
|
# the crash watcher, or about the agent it just launched to investigate
|
|
# the last crash, is a loop with a toast in it.
|
|
[[ "$program" == panama-crash-* || "$program" == panama-agent* ]] && continue
|
|
|
|
[[ -z "${reported[$program]:-}" ]] || continue
|
|
reported[$program]=1
|
|
|
|
# The toast can only offer a diagnosis if there is something to diagnose
|
|
# with. No agent, or the offer switched off, and it stays exactly the
|
|
# actionless notification it has always been.
|
|
agent="$(read_setting preferredAgent none)"
|
|
offer="$(read_setting crashDiagnoseOffer true)"
|
|
|
|
if [[ -n "$agent" && "$agent" != "none" && "$offer" != "false" && "$pid" =~ ^[0-9]+$ ]]; then
|
|
# By absolute path, not by name. The shell runs this hint, and the
|
|
# shell is started by systemd -- whose environment does not carry
|
|
# the repository's bin directory on PATH, so a bare name would
|
|
# click into "command not found".
|
|
exec_command="$(printf '%q %q %q %q %q' \
|
|
"$PANAMA_PATH/bin/panama-agent-crash" \
|
|
"$pid" "${comm:-$program}" "${exe:-unknown}" "${signal:-unknown}")"
|
|
|
|
# The hint is data, not privilege. Any process on this session bus
|
|
# could send one, and running it grants nothing a local process
|
|
# could not already do for itself.
|
|
# Same urgency as the plain report, deliberately. Making the
|
|
# clickable one critical would let a crash break through Do Not
|
|
# Disturb, which is a louder desktop than anybody asked for in
|
|
# exchange for an offer that keeps until it is read anyway.
|
|
notify-send --icon=dialog-error-symbolic --app-name=Panama \
|
|
--hint="string:panama-exec:$exec_command" \
|
|
"$program stopped unexpectedly" \
|
|
"Click to diagnose with $(agent_label "$agent")." \
|
|
2>/dev/null || true
|
|
else
|
|
notify-send --icon=dialog-error-symbolic --app-name=Panama \
|
|
"$program stopped unexpectedly" \
|
|
"It crashed and was not able to recover. System Health has the details." \
|
|
2>/dev/null || true
|
|
fi
|
|
done
|