From e723fabed3abf0c534fee66122439d4b7ce9e42d Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Sun, 23 Aug 2026 13:55:38 -0400 Subject: [PATCH] Stop the session you started, so the next one can begin Logging out of plain Hyprland left the desktop running. autostart.lua starts about ten user units, none of them children of the compositor, and the shutdown handler that was meant to stop them stopped hyprland-session.target -- a unit Fedora does not ship. systemctl says 'Unit not found', the handler reports nothing, and hyprpaper, hypridle, vicinae and the polkit agent keep running after logout. Each of them Requires=graphical-session.target, so that target stayed active with no session behind it. The symptom was not a broken Hyprland. Both uwsm and gnome-session refuse to start into a session that is already running, so 'Hyprland (uwsm-managed)' and GNOME both bounced back to the login screen, and plain Hyprland -- which makes no such check -- was the only one that worked. The session that looked healthy was the one leaving the mess, and it was the one Panama tells people not to use. Now stops graphical-session.target, which every one of those units is PartOf, so a unit added to the start handler later cannot be forgotten here. panama-crash-watch had the same leak and no PartOf at all: a journal follower per login, and a 'once per program per session' dedup that had quietly become once per boot. --- README.md | 2 +- config/dot/hypr/autostart.lua | 22 +++++- .../systemd/user/panama-crash-watch.service | 8 ++ tests/hypr/session-teardown-contract | 79 +++++++++++++++++++ 4 files changed, 109 insertions(+), 2 deletions(-) create mode 100755 tests/hypr/session-teardown-contract 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'