#!/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'