Add omarchy-crash-mute to mute and unmute one program
The mute was reachable only as `omarchy-toggle crash-ignore/<program>`, which asks whoever runs it to know the flag layout, to reduce a binary's path to the name the watcher keys on, and to have read the rule that a name climbing out of that directory writes an unrelated toggle. All of that was carried in the skill's prose, which is the wrong place for a rule that has to hold: prose is advice, and the thing being advised about is a name the crashed program chose. So it is a command now. `omarchy crash mute hyprland` silences that program, `off` lifts it, `toggle` flips it, and no argument lists what is muted. It takes the binary's path as readily as the name and reduces it the way the watcher does, so the `Executable:` line from `coredumpctl` can be handed straight to it; it refuses what is not one component of a name, so it cannot be talked into writing outside its own directory whatever it is given; and it re-reads the flag afterwards and reports what is now true rather than what was asked for. The listing counts only regular files, because that is all the watcher honours -- anything else in there would read as muted while the crashes kept arriving. A leading `--` is consumed so a program named `-h`, which the router would otherwise answer with its own help, can still be muted. The watcher gained an unrelated fix that this uncovered. Its fields are read with `IFS=$'\t'`, and tab is IFS whitespace, so an empty field collapsed into the next delimiter and shifted every field after it along one: a crash whose comm was empty had a path read as its pid and was discarded as somebody else's. A process can set its comm to nothing, so that was reachable. Empty fields now arrive as a dash like missing ones, and a dash joins the empty and dot cases that fall back to `unknown`. Co-Authored-By: Codex XHigh <noreply@openai.com>
This commit is contained in:
committed by
David Heinemeier Hansson
co-authored by
Codex XHigh
parent
eeb4206c7b
commit
ea6ee9440a
@@ -40,6 +40,7 @@ GROUP_DESCRIPTIONS[channel]="Omarchy release channel management"
|
||||
GROUP_DESCRIPTIONS[clipboard]="Clipboard helpers"
|
||||
GROUP_DESCRIPTIONS[cmd]="Command and shortcut helpers"
|
||||
GROUP_DESCRIPTIONS[config]="System configuration helpers"
|
||||
GROUP_DESCRIPTIONS[crash]="Crash notification controls"
|
||||
GROUP_DESCRIPTIONS[debug]="Diagnostics and support logs"
|
||||
GROUP_DESCRIPTIONS[finalize]="Finalize user setup"
|
||||
GROUP_DESCRIPTIONS[default]="Default application selection"
|
||||
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Silence crash notifications for one program, or list what is silenced
|
||||
# omarchy:args=[--] [<program>] [on|off|toggle]
|
||||
# omarchy:examples=omarchy crash mute | omarchy crash mute hyprland | omarchy crash mute /usr/bin/hyprland | omarchy crash mute hyprland off
|
||||
|
||||
# The flag omarchy-crash-watch reads before announcing a crash. Muting is per
|
||||
# program; Trigger > Toggle > Crash Capture is the switch for all of them.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
readonly MUTES="$HOME/.local/state/omarchy/toggles/crash-ignore"
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy crash mute [--] [<program>] [on|off|toggle]" >&2
|
||||
}
|
||||
|
||||
# Only regular files, because that is all the watcher honours: anything else in
|
||||
# there would be reported as muted while the crashes kept arriving. The dotted
|
||||
# glob is for a program legitimately called .hidden, and `.` and `..` fail the
|
||||
# same -f test that keeps them out.
|
||||
list() {
|
||||
local entry found=0
|
||||
|
||||
for entry in "$MUTES"/* "$MUTES"/.*; do
|
||||
[[ -f $entry ]] || continue
|
||||
printf '%s\n' "${entry##*/}"
|
||||
found=1
|
||||
done
|
||||
|
||||
((found)) || echo "No programs muted. Crashes all notify."
|
||||
}
|
||||
|
||||
# A program may be named -h, and the router answers that with its own help
|
||||
# before this ever runs. `omarchy crash mute -- -h` is the way through.
|
||||
[[ ${1:-} == "--" ]] && shift
|
||||
|
||||
if (($# == 0)); then
|
||||
list
|
||||
exit 0
|
||||
fi
|
||||
|
||||
program=$1
|
||||
action=${2:-on}
|
||||
|
||||
# The watcher keys the mute on the executable's basename, so accept the path it
|
||||
# reports as readily as the name, and reduce either the same way it does.
|
||||
program=${program##*/}
|
||||
|
||||
if [[ -z $program || $program == "." || $program == ".." ]]; then
|
||||
echo "Not a program name: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$action" in
|
||||
on|off|toggle) ;;
|
||||
*)
|
||||
echo "Not an action: $action" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
omarchy-toggle "crash-ignore/$program" "$action" || exit 1
|
||||
|
||||
# Report what is now true rather than what was asked for: the flag is what the
|
||||
# watcher reads, and a toggle does not say which way it went.
|
||||
if omarchy-toggle-enabled "crash-ignore/$program"; then
|
||||
echo "Muted crash notifications for $program."
|
||||
else
|
||||
echo "Crash notifications for $program are back on."
|
||||
fi
|
||||
+16
-10
@@ -48,12 +48,17 @@ announce() {
|
||||
# -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 '[(._UID // "-"),
|
||||
(.COREDUMP_COMM // "-"),
|
||||
(.COREDUMP_PID // "-"),
|
||||
(.COREDUMP_EXE // "-"),
|
||||
(.COREDUMP_SIGNAL_NAME // "-")] | @tsv' <<<"$entry" 2>/dev/null
|
||||
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
|
||||
@@ -78,10 +83,11 @@ journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
|
||||
name=${name##*/}
|
||||
|
||||
# What that leaves is not always a name. "/" leaves nothing, which is no
|
||||
# kind of array subscript and no kind of toast, and a dot component names a
|
||||
# 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.
|
||||
[[ -n $name && $name != "." && $name != ".." ]] || name=unknown
|
||||
# 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
|
||||
|
||||
@@ -89,8 +95,8 @@ journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
|
||||
[[ $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 it un-mutes with
|
||||
# `omarchy-toggle crash-ignore/<program> off` and reads with `ls -A`.
|
||||
# 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
|
||||
|
||||
@@ -98,39 +98,40 @@ dumps core every time it exits, a driver that misbehaves on this hardware. Finis
|
||||
by offering to silence crash notifications for **that one program**:
|
||||
|
||||
```bash
|
||||
omarchy-toggle 'crash-ignore/<program>' on
|
||||
omarchy-crash-mute '<program>'
|
||||
```
|
||||
|
||||
`<program>` is the `process:` name in the crash facts, verbatim. The watcher works
|
||||
that name out and then announces it, so what you were handed is already the exact
|
||||
string the mute is keyed on — do not re-derive it from `coredumpctl` when you were
|
||||
given it, because the two agree for ordinary names and not for strange ones.
|
||||
`<program>` is the `process:` name in the crash facts, or the `binary:` path —
|
||||
the command reduces a path to the same name the watcher keys on, so passing
|
||||
`/usr/lib/chromium/chromium-browser` and passing `chromium-browser` land on the
|
||||
same flag. Prefer the binary's path wherever the crash recorded one: the kernel
|
||||
truncates the process name to 15 characters and does not truncate a basename, and
|
||||
a mute on the truncated form matches nothing, forever, while looking like it
|
||||
worked.
|
||||
|
||||
A diagnosis started by hand from `omarchy agent crash <pid>` is given no name, so
|
||||
there you do have to work it out the way the watcher does: the executable's
|
||||
basename when an absolute `Executable:` was recorded, otherwise the process name
|
||||
with everything up to the last `/` dropped, and `unknown` when that leaves
|
||||
nothing, `.` or `..`. Prefer the executable — the kernel truncates the process
|
||||
name to 15 characters and does not truncate the basename, so a mute on the
|
||||
truncated one matches nothing, forever, while looking like it worked.
|
||||
That is also the answer for a diagnosis started by hand from `omarchy agent crash
|
||||
<pid>`, which is handed no name at all: give the command the `Executable:` line
|
||||
from `coredumpctl info` and let it do the reducing. Some crashes record no
|
||||
executable — pass the process name then, and `unknown` where the crash has
|
||||
neither, which is the name such a crash is announced under.
|
||||
|
||||
The name is whatever the crashed program's author chose to call a file, so handle
|
||||
it as hostile text rather than as a word. Single quotes hold a space or a `$(...)`,
|
||||
but a name containing a single quote closes them and the rest of it runs as your
|
||||
shell — escape it, or the program that just crashed chooses the command. Then
|
||||
check the flag actually arrived, which is also how you learn a name was too long
|
||||
for the filesystem to keep:
|
||||
|
||||
```bash
|
||||
omarchy-toggle-enabled 'crash-ignore/<program>' && echo muted
|
||||
```
|
||||
The name is still whatever the crashed program's author chose to call a file, so
|
||||
handle it as hostile text rather than as a word. The command refuses a name that
|
||||
is not one — it cannot be talked into writing a flag outside its own directory —
|
||||
but that is no help if the name reaches a shell unescaped first: single quotes
|
||||
hold a space or a `$(...)`, and a name containing a single quote closes them and
|
||||
runs the rest as your shell. Escape it, or the program that just crashed picks
|
||||
the command.
|
||||
|
||||
Offer it; never run it unprompted. The user may well want to keep being told.
|
||||
|
||||
Say how to undo it in the same breath, so it is not a one-way door: the same
|
||||
command with `off` un-mutes, and each mute is one file in
|
||||
`~/.local/state/omarchy/toggles/crash-ignore/`, which `ls -A` lists — the
|
||||
directory appears with the first mute, so before that there is nothing to list.
|
||||
Say how to undo it in the same breath, so it is not a one-way door — and the
|
||||
command answers both halves itself:
|
||||
|
||||
```bash
|
||||
omarchy-crash-mute '<program>' off # un-mute this one
|
||||
omarchy-crash-mute # list what is muted
|
||||
```
|
||||
|
||||
The key is a bare name, so programs sharing one share a mute, and anything run
|
||||
through an interpreter is keyed as the interpreter. Muting `python3.13` or `node`
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ Omarchy watches systemd-coredump for process crashes. When something segfaults,
|
||||
|
||||
The watching is on by default. Turn it off under _Trigger > Toggle > Crash Capture_ (or with `omarchy toggle crash-capture`) and the notifications stop; `omarchy agent crash <pid>` still works by hand.
|
||||
|
||||
Crashes can also be silenced one program at a time, which is what the diagnosis offers you at the end: `omarchy toggle 'crash-ignore/<program>' on` stops the notifications for that program only, and the same command with `off` brings them back. Use `<program>` exactly as the notification named it, in single quotes, which is what carries a name with a space in it; a name containing a quote character closes those quotes, so that one has to be escaped as well. Each mute is a file in `~/.local/state/omarchy/toggles/crash-ignore/`, so `ls -A` there shows what you've muted once you've muted something. Everything else still notifies, and the muted program still crashes — this hides the reminder, it doesn't fix anything.
|
||||
Crashes can also be silenced one program at a time, which is what the diagnosis offers you at the end. `omarchy crash mute hyprland` stops the notifications for that program only, `omarchy crash mute hyprland off` brings them back, and `omarchy crash mute` on its own lists what you've muted. It takes the binary's path as happily as its name, so `omarchy crash mute /usr/bin/hyprland` does the same thing. Quote a name with a space in it, as in `omarchy crash mute 'Some App'`. Everything else still notifies, and the muted program still crashes — this hides the reminder, it doesn't fix anything.
|
||||
|
||||
### Desktop apps
|
||||
|
||||
|
||||
@@ -122,8 +122,12 @@ run_watch() {
|
||||
fail "the watcher exited $status rather than carrying on, so a mute takes the service down with it"
|
||||
}
|
||||
|
||||
# Through the real command rather than writing the flag by hand: these assertions
|
||||
# are then the guard that the thing the diagnosis runs and the thing the watcher
|
||||
# reads have not drifted apart.
|
||||
mute() {
|
||||
HOME="$watch_home" "$ROOT/bin/omarchy-toggle" "crash-ignore/$1" "$2"
|
||||
HOME="$watch_home" PATH="$ROOT/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-crash-mute" "$1" "$2" >/dev/null
|
||||
}
|
||||
|
||||
announced() {
|
||||
@@ -214,6 +218,19 @@ for empty_comm in / a/ . ..; do
|
||||
done
|
||||
pass "a comm that strips down to nothing or a dot still announces under a name a mute can use"
|
||||
|
||||
# An empty comm is not a missing entry. Tab is IFS whitespace, so an empty field
|
||||
# collapses and every field after it shifts along one -- the pid becomes a path,
|
||||
# the crash reads as somebody else's, and it is dropped without a word.
|
||||
reset_entries
|
||||
crash_entry "" -
|
||||
crash_entry nautilus /usr/bin/nautilus
|
||||
run_watch
|
||||
announced unknown ||
|
||||
fail "a crash whose comm is empty is dropped instead of announced, because the empty field shifted every field after it"
|
||||
announced nautilus ||
|
||||
fail "an empty comm derails the rest of the journal entry"
|
||||
pass "an empty comm is announced rather than parsed into the next field"
|
||||
|
||||
# Only "." and ".." are special. A leading dot is an ordinary filename, and
|
||||
# folding those into the fallback would have one program's mute silence another.
|
||||
for dotted_comm in .hidden ...; do
|
||||
@@ -235,12 +252,135 @@ run_watch
|
||||
pass "the fallback name can be muted like any other"
|
||||
mute unknown off
|
||||
|
||||
# What omarchy-crash-mute does on its own. That it agrees with the watcher is
|
||||
# already covered above, which drives it for every mute it makes.
|
||||
mute_home="$TMPDIR/mute-home"
|
||||
mkdir -p "$mute_home"
|
||||
|
||||
crash_mute() {
|
||||
HOME="$mute_home" PATH="$ROOT/bin:$PATH" "$ROOT/bin/omarchy-crash-mute" "$@"
|
||||
}
|
||||
|
||||
mute_flag() {
|
||||
[[ $1 == "--" ]] && shift
|
||||
printf '%s' "$mute_home/.local/state/omarchy/toggles/crash-ignore/$1"
|
||||
}
|
||||
|
||||
crash_mute | grep -Fq "No programs muted" ||
|
||||
fail "an empty mute list prints nothing, so a user cannot tell it from a broken command"
|
||||
pass "the command says so when nothing is muted"
|
||||
|
||||
crash_mute hyprland >/dev/null
|
||||
crash_mute | grep -Fqx hyprland ||
|
||||
fail "a muted program is missing from the list, so a mute cannot be found again to lift it"
|
||||
pass "the command lists what it muted"
|
||||
|
||||
# The watcher keys on the basename, so the command has to take the path a crash
|
||||
# recorded and land on the same flag the watcher will look for.
|
||||
crash_mute /usr/lib/chromium/chromium-browser >/dev/null
|
||||
[[ -f $(mute_flag chromium-browser) ]] ||
|
||||
fail "a binary's path is muted verbatim rather than by name, so the watcher never sees that flag"
|
||||
pass "the command reduces a path to the name the watcher checks"
|
||||
|
||||
crash_mute hyprland off >/dev/null
|
||||
[[ ! -f $(mute_flag hyprland) ]] ||
|
||||
fail "off leaves the program muted, making the mute a one-way door"
|
||||
pass "the command un-mutes"
|
||||
|
||||
# Muting is not flipping. The diagnosis offers this on a program the user may
|
||||
# already have muted, and asking for a mute twice has to leave it muted.
|
||||
crash_mute hyprland >/dev/null
|
||||
crash_mute hyprland >/dev/null
|
||||
[[ -f $(mute_flag hyprland) ]] ||
|
||||
fail "muting an already-muted program un-mutes it, so offering the mute a second time turns it back on"
|
||||
pass "asking to mute twice leaves it muted"
|
||||
|
||||
# A program may legitimately be called .hidden, and a mute nobody can see is a
|
||||
# mute nobody can lift.
|
||||
crash_mute .hidden >/dev/null
|
||||
crash_mute | grep -Fqx .hidden ||
|
||||
fail "a mute on a dotted name is missing from the list, so it can never be found and lifted"
|
||||
pass "the list shows a name that begins with a dot"
|
||||
|
||||
# It turns what it is given into a path, so it has to refuse whatever is not one
|
||||
# component of one.
|
||||
for bad_name in . .. /; do
|
||||
! crash_mute "$bad_name" >/dev/null 2>&1 ||
|
||||
fail "'$bad_name' is taken as a program name, and the flag that writes is not one the watcher will ever read"
|
||||
done
|
||||
pass "the command refuses a name that is not a name"
|
||||
|
||||
! crash_mute hyprland sideways >/dev/null 2>&1 ||
|
||||
fail "an action it does not know is treated as a mute, so a typo silences a program"
|
||||
pass "the command refuses an action it does not know"
|
||||
|
||||
# And says what it refused, or the user retypes the same thing. Captured rather
|
||||
# than piped: the command exits non-zero here, which pipefail would surface as
|
||||
# the pipeline's status and read as a failed assertion.
|
||||
refusal=$(crash_mute hyprland sideways 2>&1) || true
|
||||
grep -Fq "Not an action" <<<"$refusal" ||
|
||||
fail "an unknown action is refused without naming it, leaving the user nothing to correct"
|
||||
pass "the command names the action it refused"
|
||||
|
||||
crash_mute ../bar-off >/dev/null
|
||||
[[ ! -e "$mute_home/.local/state/omarchy/toggles/bar-off" ]] ||
|
||||
fail "a name that climbs out writes a sibling toggle, so muting a crash could turn off the bar instead"
|
||||
pass "the command cannot be talked into writing outside crash-ignore/"
|
||||
|
||||
# A program may be called -h, and the router answers that with its own help
|
||||
# before the command runs. A leading -- is the way through, so it has to be
|
||||
# consumed rather than taken for the program name.
|
||||
crash_mute -- -h >/dev/null 2>&1 ||
|
||||
fail "a leading -- is refused rather than consumed, so a program named like a flag cannot be muted at all"
|
||||
[[ -f $(mute_flag -- -h) ]] ||
|
||||
fail "a leading -- is taken for the program name, so muting -h mutes something else"
|
||||
pass "a leading -- lets a program named like a flag be muted"
|
||||
|
||||
# toggle is advertised, so it has to flip both ways rather than quietly mute.
|
||||
crash_mute toggler off >/dev/null
|
||||
crash_mute toggler toggle >/dev/null
|
||||
[[ -f $(mute_flag toggler) ]] ||
|
||||
fail "toggle does not mute an un-muted program"
|
||||
crash_mute toggler toggle >/dev/null
|
||||
[[ ! -f $(mute_flag toggler) ]] ||
|
||||
fail "toggle mutes but never un-mutes, so the advertised action only goes one way"
|
||||
pass "toggle flips a mute both ways"
|
||||
|
||||
# The listing means what the watcher means, and the watcher honours a regular
|
||||
# file. Anything else in there is not a mute, however much it looks like one.
|
||||
mkdir -p "$(mute_flag notactuallymuted)"
|
||||
! crash_mute | grep -Fqx notactuallymuted ||
|
||||
fail "a directory is reported as muted while that program's crashes keep arriving"
|
||||
pass "the listing counts only the flags the watcher honours"
|
||||
rmdir "$(mute_flag notactuallymuted)"
|
||||
|
||||
# A mute that could not be written must not be reported as one. Without this the
|
||||
# command can print success for a flag that was never created.
|
||||
failing_bin="$TMPDIR/failing-bin"
|
||||
mkdir -p "$failing_bin"
|
||||
cat >"$failing_bin/omarchy-toggle" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 1
|
||||
SH
|
||||
chmod +x "$failing_bin/omarchy-toggle"
|
||||
|
||||
status=0
|
||||
refusal=$(HOME="$mute_home" PATH="$failing_bin:$ROOT/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-crash-mute" hyprland 2>&1) || status=$?
|
||||
(( status != 0 )) ||
|
||||
fail "a mute that could not be written exits zero, so nothing downstream learns it failed"
|
||||
! grep -Fq "Muted crash notifications" <<<"$refusal" ||
|
||||
fail "a mute that could not be written still reports success, so the user believes a program is silenced when it is not"
|
||||
pass "a mute that could not be written is not reported as one"
|
||||
|
||||
skill="$ROOT/default/agents/skills/diagnose-crash/SKILL.md"
|
||||
grep -Fq 'crash-ignore/' "$skill" ||
|
||||
fail "the diagnosis no longer offers the mute under the name the watcher reads, so the two have drifted apart"
|
||||
grep -Fq 'crash-ignore/$name' "$ROOT/bin/omarchy-crash-watch" ||
|
||||
fail "the watcher no longer reads the flag the diagnosis offers to write"
|
||||
pass "the diagnosis and the watcher name the same flag"
|
||||
grep -Fq 'omarchy-crash-mute' "$skill" ||
|
||||
fail "the diagnosis no longer names the command that mutes, so the offer it makes cannot be carried out"
|
||||
pass "the diagnosis names the command that mutes"
|
||||
|
||||
grep -Fq 'GROUP_DESCRIPTIONS[crash]' "$ROOT/bin/omarchy" ||
|
||||
fail "the crash group has no description, so the router lists a group it cannot describe"
|
||||
pass "the crash group is described in the router"
|
||||
|
||||
run_node_test <<'JS'
|
||||
const fs = require('fs')
|
||||
|
||||
Reference in New Issue
Block a user