diff --git a/README.md b/README.md index 7007edf..bf775d3 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ docs/ Settings reference, and the design specs behind the work ## Tests -158 of them, under `tests/`. Run the lot, or a subset by pattern: +159 of them, under `tests/`. Run the lot, or a subset by pattern: ```sh panama test # everything diff --git a/config/dot/hypr/autostart.lua b/config/dot/hypr/autostart.lua index f495b77..032502e 100644 --- a/config/dot/hypr/autostart.lua +++ b/config/dot/hypr/autostart.lua @@ -91,8 +91,28 @@ hl.on("hyprland.start", function() -- session on its own. Starting it here as well would give you two trays. end) +-- Tear down what the start handler brought up. +-- +-- This stopped `hyprland-session.target`, which does not exist on Fedora -- +-- systemctl reports "Unit not found" and the handler achieves nothing. The +-- units started above are not children of the compositor, so nothing else +-- stopped them either: hyprpaper, hypridle, vicinae and the polkit agent kept +-- running after logout, and each one Requires=graphical-session.target, so +-- that target stayed active with no session behind it. +-- +-- The next login then failed. Both uwsm and gnome-session refuse to start into +-- a session that is already running ("A compositor or graphical-session* +-- target is already active!"), so "Hyprland (uwsm-managed)" and GNOME both +-- bounced straight back to the login screen while plain Hyprland -- which +-- makes no such check -- kept working. The desktop appeared to be the only one +-- that functioned, when it was the one leaving the mess. +-- +-- Stopping graphical-session.target is enough on its own: every unit above is +-- PartOf= it, so they come down with it, and it carries StopWhenUnneeded=yes. +-- It is stopped rather than the units listed individually so that a unit added +-- to the start handler later cannot be forgotten here. hl.on("hyprland.shutdown", function() - hl.exec_cmd("systemctl --user stop hyprland-session.target") + hl.exec_cmd("systemctl --user stop graphical-session.target") end) return true diff --git a/config/local/share/systemd/user/panama-crash-watch.service b/config/local/share/systemd/user/panama-crash-watch.service index 2567b0b..4f23f4e 100644 --- a/config/local/share/systemd/user/panama-crash-watch.service +++ b/config/local/share/systemd/user/panama-crash-watch.service @@ -5,6 +5,14 @@ Documentation=https://github.com/gibbyb/Panama # the same reason as every other Panama unit: graphical-session.target is # active under GNOME too, and this reports through the Hyprland shell's # notification server. +# +# PartOf, so it stops with the session that started it. Without it this +# outlived logout -- a journal follower per login, accumulating -- and its +# "once per program per session" dedup silently became once per boot, which is +# the opposite of what it promises: the second crash of the day went unreported +# because the first session had already spoken. +PartOf=graphical-session.target +After=graphical-session.target [Service] Type=simple diff --git a/tests/hypr/session-teardown-contract b/tests/hypr/session-teardown-contract new file mode 100755 index 0000000..d5446b1 --- /dev/null +++ b/tests/hypr/session-teardown-contract @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +# What the session starts, the session must stop. +# +# autostart.lua starts about ten user units on hyprland.start. None of them is +# a child of the compositor, so when Hyprland exits they keep running -- and +# each one carries Requires=graphical-session.target, so that target stays +# active with no session behind it. +# +# The shutdown handler was supposed to prevent exactly that, and did not: it +# stopped `hyprland-session.target`, a unit Fedora does not ship. systemctl +# reports "Unit not found" and exits, the handler reports nothing, and the +# stale session survives to the login screen. +# +# The symptom was not a broken Hyprland. It was that "Hyprland (uwsm-managed)" +# and GNOME both bounced back to the login screen -- both refuse to start into +# a session that is already running -- while plain Hyprland, which makes no +# such check, kept working. The one session that appeared healthy was the one +# doing the damage. +# +# Pinned here because the failure is silent in both directions: a handler that +# stops nothing looks identical to one that works, until the next login. + +set -uo pipefail + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +autostart="$repo_dir/config/dot/hypr/autostart.lua" + +findings=() +note() { findings+=("$1"); } + +[[ -r "$autostart" ]] || { printf 'session teardown contract: no autostart.lua\n' >&2; exit 1; } + +# Comments stripped: the header above the handler explains the whole bug at +# length, and matching that would be matching documentation rather than code. +uncommented() { grep -vE '^\s*--' "$autostart"; } + +shutdown_body="$(uncommented | sed -n '/hl\.on("hyprland\.shutdown"/,/^end)/p')" + +[[ -n "$shutdown_body" ]] \ + || note 'there is no hyprland.shutdown handler, so nothing tears the session down' + +# ── It must stop something that exists ────────────────────────────────────── + +grep -q 'hyprland-session\.target' <<<"$shutdown_body" \ + && note 'the shutdown handler stops hyprland-session.target, which Fedora does not ship -- it is a no-op' + +grep -q 'stop graphical-session\.target' <<<"$shutdown_body" \ + || note 'the shutdown handler does not stop graphical-session.target, so the units outlive the session' + +# ── The mechanism it relies on ────────────────────────────────────────────── +# +# Stopping the target only takes the units with it while they are PartOf= it. +# A unit shipped without that line would survive the teardown and hold the +# target back up, which is the original bug wearing a different hat. + +units_dir="$repo_dir/config/local/share/systemd/user" +if [[ -d "$units_dir" ]]; then + for unit in "$units_dir"/*.service; do + [[ -e "$unit" ]] || continue + name="$(basename "$unit")" + + # Only the ones the session starts and expects to outlive nothing. A + # oneshot notifier exits on its own and needs no teardown. + grep -q "start .*$name" "$autostart" || continue + grep -qE '^\s*Type\s*=\s*oneshot' "$unit" && continue + + grep -qE '^\s*PartOf\s*=.*graphical-session\.target' "$unit" \ + || note "$name is started by the session but is not PartOf graphical-session.target, so it survives logout" + done +fi + +if (( ${#findings[@]} > 0 )); then + printf 'session teardown contract: %d finding(s)\n' "${#findings[@]}" >&2 + printf ' - %s\n' "${findings[@]}" >&2 + exit 1 +fi + +printf 'session teardown contract: PASS\n'