Files
omarchy/bin/omarchy-crash-watch
T
ZacharyZhang-NY a9b39710a2 Add Simplified Chinese as a system language
The installer asks for the language before the keyboard, and
Setup > Language (omarchy language en|zh) switches it later. Under
zh_CN the menu reads its labels from omarchy-menu.zh-cn.jsonc, the
shell from I18nZh.js through I18n.tr, and notifications are worded
by their sender through omarchy-text, which reads the same table.
User text is never translated.
2026-10-12 02:56:40 -04:00

110 lines
4.7 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Watch for process crashes and offer an AI diagnosis
# omarchy:hidden=true
# systemd-coredump journals every core dump under a known MESSAGE_ID with
# structured COREDUMP_* fields, which carry more than the core filenames do.
set -uo pipefail
# See systemd.journal-fields(7).
readonly COREDUMP_MESSAGE_ID=fc2e22bc6ee647b6b90729ab34a250b1
# nf-md-robot_dead, escaped so this file reads without a Nerd Font.
readonly CRASH_GLYPH=$'\U000f16a1'
# Crash loops dump core repeatedly, so announce each program at most once a
# window.
readonly dedupe_seconds=${OMARCHY_CRASH_DEDUPE_SECONDS:-60}
# Extended regex of process names never worth announcing.
readonly ignore_pattern=${OMARCHY_CRASH_IGNORE:-}
declare -A last_notified
announce() {
local comm=$1 pid=$2 exe=$3 signal=$4
# The shell owns org.freedesktop.Notifications, so a shell crash takes the
# notification server down with it and a toast sent into that gap is lost.
# Wait for the restarted shell to claim the name again: the crash least
# likely to be delivered is the one most worth reporting.
omarchy-notification-wait || return 1
# --exec rather than a libnotify action: the shell runs clicks from its own
# hint and never emits ActionInvoked. Keeps the default "omarchy-action" app
# name, the only one shouldBypassDnd() lets through. Crash details ride as
# discrete argv words, so a hostile process name can't be reparsed as a
# command. --exec consumes the rest of the line, so it comes last.
omarchy-notification-send \
--urgency critical \
--glyph "$CRASH_GLYPH" \
"$(omarchy-text "Process crashed: %1" "$comm")" \
"$(omarchy-text "Click to diagnose with AI")" \
--exec omarchy-agent-crash "$pid" "$comm" "$exe" "$signal"
}
# -n 0 so a restart does not re-announce crashes already dealt with.
journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
while IFS= read -r entry; do
# A dash for a field that is empty as well as one that is missing: tab is
# IFS whitespace, so an empty field collapses into the next delimiter and
# every field after it shifts along one. A process can set its own comm to
# nothing, and that crash used to be read as somebody else's and dropped.
IFS=$'\t' read -r uid comm pid exe signal < <(
jq -r 'def field: if . == null or . == "" then "-" else . end;
[(._UID | field),
(.COREDUMP_COMM | field),
(.COREDUMP_PID | field),
(.COREDUMP_EXE | field),
(.COREDUMP_SIGNAL_NAME | field)] | @tsv' <<<"$entry" 2>/dev/null
)
[[ $pid =~ ^[0-9]+$ ]] || continue
# The toast only offers a diagnosis, so it has nothing to offer until an
# agent is chosen. Checked per crash, not at startup, so picking one takes
# effect without restarting this service.
[[ -n $(omarchy-default-agent) ]] || continue
# Only this user's crashes; a daemon dumping core is a sysadmin's problem.
[[ $uid =~ ^[0-9]+$ ]] || continue
((uid == UID)) || continue
# comm is truncated to 15 characters, so prefer the executable's basename.
name=$comm
[[ $exe == /* ]] && name=${exe##*/}
# A process can set its own comm to anything prctl takes, slashes included,
# and a crash with no recorded executable falls back to it. The mute below
# turns this name into a path, so keep it one component: a crash must not
# reach a flag outside crash-ignore/, nor have a diagnosis write one there.
name=${name##*/}
# What that leaves is not always a name. "/" leaves nothing, which is no
# kind of array subscript and no kind of toast; a dot component names a
# directory rather than a flag, so a mute on it would touch that directory
# and then never match; and a dash is what the read above puts there when
# the crash recorded no name at all.
[[ -n $name && $name != "-" && $name != "." && $name != ".." ]] || name=unknown
[[ -n $ignore_pattern && $name =~ $ignore_pattern ]] && continue
# Never announce our own machinery, or it notifies about itself.
[[ $name == omarchy-crash-* || $name == omarchy-agent-* ]] && continue
# Muted at the end of a diagnosis, when the user was offered it and said
# yes. A flag per program rather than one list, so omarchy-crash-mute can
# lift one without reading, rewriting and re-parsing the rest.
omarchy-toggle-enabled "crash-ignore/$name" && continue
now=$EPOCHSECONDS
(((now - ${last_notified[$name]:-0}) < dedupe_seconds)) && continue
# Only a delivered toast starts the dedupe window. A failed send that
# counted would suppress the rest of a crash loop for a minute, and
# `journalctl -n 0` never replays what was missed.
announce "$name" "$pid" "$exe" "$signal" && last_notified[$name]=$now
done