Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ fi
f=$(systemctl --user --failed --no-legend --plain 2>/dev/null | awk '{print $1}' \
| sed 's/^macarchy-failed@\(.*\)\.service$/\1/' | sort -u | paste -sd' ' -)
[[ -z $f ]] && ok "no failed user units" || bad "failed user units: $f"
# macarchy-auto-appearance now exits 0 when it cannot compute a sun: a deliberate
# no-op must not park its unit in --failed for ever (macarchy-core#12). That was
# also the only thing a MISCONFIGURED laptop had to tell it, so the doctor asks
# directly instead. Gated on omarchy, because without it the theme switch could
# not happen at all -- false on a CI runner, true on any real machine.
if command -v omarchy >/dev/null 2>&1 && command -v macarchy-auto-appearance >/dev/null 2>&1; then
aa=$(macarchy-auto-appearance status 2>/dev/null)
if [[ $aa == *"enabled=yes"*"error=sun"* || $aa == *"error=sun"*"enabled=yes"* ]]; then
bad "auto-appearance is on but cannot compute the sun (set coordinates; see macarchy-sun)"
else
ok "auto-appearance can decide a theme"
fi
else
skip "auto-appearance sun (no omarchy on this machine)"
fi

# A muted check is worse than none, so the doctor checks that it can speak.
[[ -e $HOME/.config/systemd/user/macarchy-failed@.service ]] \
&& ok "failure notifier" || bad "failure notifier (macarchy-install/install.sh)"
Expand Down Expand Up @@ -134,6 +150,16 @@ if [[ -n ${WAYLAND_DISPLAY:-} ]]; then
broken=${broken% }
[[ -z $broken ]] && ok "Touch Bar modules loaded" \
|| bad "Touch Bar module failed to load: $broken (re-run install.sh in the repo that ships it)"
# Same as auto-appearance above: bar-contrast now exits 0 when it has
# nothing to sample, so a session that is missing grim/magick/hyprctl/jq
# would otherwise say nothing at all. Only meaningful with a session, which
# is exactly what this block already has.
if command -v macarchy-bar-contrast >/dev/null 2>&1; then
bc=$(macarchy-bar-contrast status 2>&1)
[[ $bc == *"nothing to sample"* ]] \
&& bad "bar-contrast has nothing to sample: ${bc#*: }" \
|| ok "bar-contrast can sample the screen"
fi
pgrep -f "macarchy-als daemon" >/dev/null && ok "macarchy-als" || bad "macarchy-als not running"
pgrep -f "macarchy-pinch" >/dev/null && ok "macarchy-pinch" || bad "macarchy-pinch not running"
# The tank remembers off across reboots (omarchy-aquarium-toggle:30, an
Expand Down
31 changes: 18 additions & 13 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,24 @@ fi

# Ordered before the themes on purpose: its installer only seeds a config when
# there is none, and the wallpapers it names are the ones apple-glass ships.
# Moved ahead of macos-dynamic-wallpaper: that step STARTS its unit, and the unit
# reads the theme's wallpapers out of ~/.config/omarchy/themes/<t>/backgrounds/
# (macos-dynamic-wallpaper#5). A oneshot that fails once stays failed, so the
# files have to be on disk BEFORE it first runs. This half needs no omarchy --
# it is an rsync; the `omarchy theme set` half stays where it was, further down.
say "Installing the apple-glass themes"
for t in apple-glass apple-glass-light; do
src="$MACARCHY_DIR/$t"
dst="$HOME/.config/omarchy/themes/$t"
if [[ -d $src ]]; then
mkdir -p "$dst"
rsync -a --delete --exclude .git "$src/" "$dst/"
note "$t synced to ~/.config/omarchy/themes"
else
warn "$t repo missing"
fi
done

say "Installing macos-dynamic-wallpaper (time-of-day backgrounds)"
if [[ -x $MACARCHY_DIR/macos-dynamic-wallpaper/install.sh ]]; then
(cd "$MACARCHY_DIR/macos-dynamic-wallpaper" && ./install.sh) \
Expand Down Expand Up @@ -374,19 +392,6 @@ LUA

# ---------------------------------------------------------------- themes

say "Installing the apple-glass themes"
for t in apple-glass apple-glass-light; do
src="$MACARCHY_DIR/$t"
dst="$HOME/.config/omarchy/themes/$t"
if [[ -d $src ]]; then
mkdir -p "$dst"
rsync -a --delete --exclude .git "$src/" "$dst/"
note "$t synced to ~/.config/omarchy/themes"
else
warn "$t repo missing"
fi
done

# The applied theme is a copy; re-apply so edits actually take. On a box
# that never chose a theme from this pair, apply the dark one.
if command -v omarchy-theme-set >/dev/null || command -v omarchy >/dev/null; then
Expand Down
6 changes: 6 additions & 0 deletions tests/clean-machine.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ systemctl --user show -p Version --value >/dev/null 2>&1 || {
exit 1
}

# On the way out, whatever the exit status, say WHY every failed user unit failed.
# It has to be a trap: install.sh runs doctor.sh ITSELF, so a MISS reds the run from
# inside the `|| exit 1` below and there is no "after install, before doctor" moment
# to hook. `|| true` and the saved status keep the diagnostics from deciding the run.
trap 'rc=$?; bash tests/dump-failed-units.sh || true; exit $rc' EXIT

./install.sh || exit 1 # it runs doctor.sh itself at the end, so a MISS reds here
sha256sum "$HOME/.config/hypr/autostart.lua" "$HOME/.config/hypr/bindings.lua" >"$MACARCHY_DIR/lua.sha"

Expand Down
47 changes: 47 additions & 0 deletions tests/dump-failed-units.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/bin/bash
# Say WHY every failed user unit failed. Read-only; prints nothing when nothing failed.
#
# clean-machine.sh could only ever report THAT units failed: install.sh runs
# doctor.sh itself, so a MISS reds the run from inside install.sh and the journals
# that would explain it are never read. Three units sat unexplained on #9 for a day
# because of it.
#
# Its own script, not a function in clean-machine.sh: that script sudo-writes into
# /etc and /usr/local/bin, so nothing can exercise it in a hermetic suite, while
# this is pure stdout over two stubbable commands (tests/test_clean_machine_journal.sh).
set -uo pipefail

# macarchy-failed@<unit>.service is the NOTIFIER for <unit> -- its own journal says
# only that it fired. Fold it onto its target, the same sed doctor.sh:63 uses, or
# the dump explains the messenger instead of the message.
dump() {
echo "=== journal: $1 ==="
# `|| true`: a unit with no journal at all must not red the run -- the dump
# is diagnostics, never an assertion.
journalctl --user -u "$1" --no-pager -n 40 2>&1 || true
}

systemctl --user --failed --no-legend --plain 2>/dev/null | awk '{print $1}' | sort -u \
| while read -r raw; do
[[ -n $raw ]] || continue
# The target first -- that is the message. But when the NOTIFIER instance
# is itself what failed, its own journal is the only one that says why,
# and folding it away left a block that explained nothing.
# Strip ONLY when the prefix is really there: `${raw%.service}` applied
# unconditionally turns a plain `a.service` into `a` and dumps it twice.
if [[ $raw == macarchy-failed@* ]]; then
target=${raw#macarchy-failed@}; target=${target%.service}
else
target=$raw
fi
dump "$target"
# An `if`, not `[[ … ]] && dump`: as the LAST statement of the loop body
# that compound returns 1 whenever the condition is false, which becomes
# the pipeline's status and then the script's. This is diagnostics; it
# must never be the reason a run goes red.
if [[ $raw != "$target" ]]; then
dump "$raw"
fi
done

exit 0
72 changes: 72 additions & 0 deletions tests/test_clean_machine_journal.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/bin/bash
# tests/test_clean_machine_journal.sh — the failed-unit journal dump.
#
# clean-machine.sh reports THAT user units failed and never why: it is install.sh
# that runs doctor.sh, so its MISS reds the run from inside, and the journals that
# would explain it are never read. Three units sat unexplained on #9 for exactly
# that reason.
#
# The seam is tests/dump-failed-units.sh's stdout, NOT clean-machine.sh's: that
# script sudo-writes into /etc and /usr/local/bin and enables linger, so it can
# never run in a hermetic suite. The dump lives in its own script so it CAN.
set -uo pipefail
cd "$(dirname "$0")/.."

TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
mkdir -p "$TMP/bin"; export PATH="$TMP/bin:$PATH"

fails=0
check() { local name=$1; shift; if "$@"; then echo "ok $name"; else echo "FAIL $name"; fails=$((fails+1)); fi; }

# systemctl replays whatever the case wrote; journalctl echoes which unit it was asked for.
cat > "$TMP/bin/systemctl" <<'STUB'
#!/bin/bash
[[ $* == *--failed* ]] || exit 1
cat "$TMP_FAILED"
STUB
cat > "$TMP/bin/journalctl" <<'STUB'
#!/bin/bash
for a in "$@"; do [[ $prev == -u ]] && echo "MARKER body of $a"; prev=$a; done
STUB
chmod +x "$TMP/bin"/*
export TMP_FAILED="$TMP/failed"

# --- (a) one failed unit: its name AND its journal reach stdout ---------------
echo 'macarchy-auto-appearance.service loaded failed failed Some daemon' > "$TMP_FAILED"
out=$(./tests/dump-failed-units.sh 2>&1)
check "(a) names the unit" grep -q 'macarchy-auto-appearance.service' <<<"$out"
check "(a) carries its journal" grep -q 'MARKER body of macarchy-auto-appearance.service' <<<"$out"

# --- (b) nothing failed: no dump at all ---------------------------------------
: > "$TMP_FAILED"
out=$(./tests/dump-failed-units.sh 2>&1)
check "(b) silence when nothing failed" [ -z "$out" ]

# --- (c) the notifier instance folds onto its target, like doctor.sh:63 -------
# macarchy-failed@macarchy-touchbar.service.service is the NOTIFIER for
# macarchy-touchbar.service. Dumping the notifier's own journal says nothing;
# doctor.sh already folds the pair into one name and so must this.
echo 'macarchy-failed@macarchy-touchbar.service.service loaded failed failed Notifier' > "$TMP_FAILED"
out=$(./tests/dump-failed-units.sh 2>&1)
check "(c) folds onto the target" grep -q 'MARKER body of macarchy-touchbar.service' <<<"$out"
# ...and the notifier's OWN journal too: when the instance is what failed, that
# is the only place saying why. The target still comes first -- it is the message.
check "(c) dumps the notifier as well" grep -q 'MARKER body of macarchy-failed@macarchy-touchbar.service.service' <<<"$out"
check "(c) target before notifier" bash -c '[ "$(grep -n "MARKER body of macarchy-touchbar.service" <<<"$1" | cut -d: -f1)" -lt "$(grep -n "MARKER body of macarchy-failed@" <<<"$1" | cut -d: -f1)" ]' _ "$out"

# --- (d) several failed units: one block each ---------------------------------
printf '%s\n' 'a.service loaded failed failed A' 'b.service loaded failed failed B' > "$TMP_FAILED"
out=$(./tests/dump-failed-units.sh 2>&1)
check "(d) one block per unit" [ "$(grep -c 'MARKER body of' <<<"$out")" = 2 ]

# --- (e) a unit with no journal must not kill the run -------------------------
echo 'c.service loaded failed failed C' > "$TMP_FAILED"
printf '#!/bin/bash\nexit 1\n' > "$TMP/bin/journalctl" # journalctl fails outright
./tests/dump-failed-units.sh >/dev/null 2>&1; rc=$?
# `$?` must be captured FIRST: inside `check "..." [ "$?" = 0 ]` it expands while
# the argument list is built, so the assertion would silently retarget to
# whatever command happened to run just before it.
check "(e) survives a journalctl failure" [ "$rc" = 0 ]

(( fails == 0 )) && echo "all ok" || echo "$fails failed"
exit $(( fails > 0 ))
81 changes: 81 additions & 0 deletions tests/test_doctor_noop_signals.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/bin/bash
# tests/test_doctor_noop_signals.sh — the checks that replaced a failed unit.
#
# macarchy-core#12 made macarchy-auto-appearance and macarchy-bar-contrast exit 0
# when they deliberately do nothing, so a oneshot no longer parks itself in
# `systemctl --user --failed` for ever. That was also the only signal a
# MISCONFIGURED laptop had, so the doctor has to ask the two tools directly --
# and a check that can only ever say "ok" would be worse than the unit it
# replaced. These cases exist to prove each one can still say MISS.
set -uo pipefail
cd "$(dirname "$0")/.."

TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
export HOME="$TMP/home" XDG_STATE_HOME="$TMP/state"
mkdir -p "$HOME" "$TMP/bin"; export PATH="$TMP/bin:$PATH"

fails=0
check() { local name=$1; shift; if "$@"; then echo "ok $name"; else echo "FAIL $name"; fails=$((fails+1)); fi; }

for stub in pgrep omarchy-aquarium-toggle systemctl journalctl; do
printf '#!/bin/bash\nexit 1\n' > "$TMP/bin/$stub"
done
printf '#!/bin/bash\nexit 0\n' > "$TMP/bin/omarchy"
printf '#!/bin/bash\ncat "$AA_STATUS"\n' > "$TMP/bin/macarchy-auto-appearance"
printf '#!/bin/bash\ncat "$BC_STATUS"\n' > "$TMP/bin/macarchy-bar-contrast"
chmod +x "$TMP/bin"/*
export AA_STATUS="$TMP/aa" BC_STATUS="$TMP/bc"
echo 'sample=#003575 verdict=light' > "$BC_STATUS"

# --- auto-appearance: on but sunless is a MISS ------------------------------
echo 'mode=solar enabled=yes error=sun' > "$AA_STATUS"
out=$(WAYLAND_DISPLAY= ./doctor.sh 2>&1)
check "sunless-but-enabled is a MISS" grep -q 'MISS.*cannot compute the sun' <<<"$out"
check "and it says how to fix it" grep -q 'set coordinates' <<<"$out"

# --- auto-appearance: a normal answer is ok ---------------------------------
echo 'mode=solar enabled=yes want=dark sunrise=06:57 sunset=20:15' > "$AA_STATUS"
out=$(WAYLAND_DISPLAY= ./doctor.sh 2>&1)
check "a working sun is ok" grep -q 'ok.*auto-appearance can decide' <<<"$out"
check "and not also a MISS" bash -c '! grep -q "cannot compute the sun" <<<"$1"' _ "$out"

# --- auto-appearance: disabled is not a MISS --------------------------------
# error=sun with the timer OFF is nobody's problem: the user turned it off.
echo 'mode=solar enabled=no error=sun' > "$AA_STATUS"
out=$(WAYLAND_DISPLAY= ./doctor.sh 2>&1)
check "sunless but switched off is not a MISS" bash -c '! grep -q "cannot compute the sun" <<<"$1"' _ "$out"

# --- no omarchy: skipped with a reason, never a silent pass -----------------
# Removing the stub is not enough: the developer running this suite HAS a real
# omarchy on PATH, and `command -v` would find it whatever $TMP/bin holds. So
# build a PATH with every directory containing an omarchy dropped -- on a CI
# runner that is a no-op, on a laptop it is the whole point of the case.
without_omarchy() {
local d out= dirs
IFS=: read -ra dirs <<<"$PATH"
for d in "${dirs[@]}"; do
[[ -x $d/omarchy ]] && continue
out="${out:+$out:}$d"
done
printf '%s' "$out"
}
echo 'mode=solar enabled=yes error=sun' > "$AA_STATUS"
rm -f "$TMP/bin/omarchy"
out=$(PATH="$(without_omarchy)" WAYLAND_DISPLAY= ./doctor.sh 2>&1)
check "no omarchy skips, not passes" grep -q 'skip.*auto-appearance sun' <<<"$out"
check "and names why" grep -q 'no omarchy on this machine' <<<"$out"
check "and does not MISS" bash -c '! grep -q "cannot compute the sun" <<<"$1"' _ "$out"
printf '#!/bin/bash\nexit 0\n' > "$TMP/bin/omarchy"; chmod +x "$TMP/bin/omarchy"

# --- bar-contrast: nothing to sample, inside a session, is a MISS -----------
echo 'macarchy-bar-contrast: grim missing; nothing to sample' > "$BC_STATUS"
out=$(WAYLAND_DISPLAY=wayland-0 ./doctor.sh 2>&1)
check "nothing to sample is a MISS" grep -q 'MISS.*bar-contrast has nothing to sample' <<<"$out"
check "and quotes the reason" grep -q 'grim missing' <<<"$out"

# --- bar-contrast: no session, no claim -------------------------------------
out=$(WAYLAND_DISPLAY= ./doctor.sh 2>&1)
check "no session: bar-contrast unasked" bash -c '! grep -q "bar-contrast" <<<"$1"' _ "$out"

(( fails == 0 )) && echo "all ok" || echo "$fails failed"
exit $(( fails > 0 ))
Loading