Say out loud that the portal backend is crash-looping

xdg-desktop-portal-hyprland 1.4.1 segfaults in its own shutdown path:
exit() runs the CPortalManager destructor, which tears down
wlr-foreign-toplevel handles and xdg-output proxies after the display
has gone, and dies in wl_map_insert_at. systemd restarts it, so the
desktop keeps working and the only symptom is screen sharing dropping
mid-session and the occasional document-portal casualty.

Sixty crashes in one day left no trace anywhere a person would look. It
took reading coredumpctl to find, and nobody reads coredumpctl on a
desktop that appears to be working -- which is exactly the shape of
problem the health page exists for. On this machine the new check
reported twenty-eight in the last day the moment it was wired up.

Reported rather than repaired, and it says so. There is nothing to
restart that systemd has not already restarted, and no newer build in
the Hyprland COPR to install: the fix is upstream. A button that did
nothing would be worse than a sentence that explains.

Below five in a day stays green, because an upstream bug that only
bites on shutdown will occasionally bite and a health page that is
never green teaches people to ignore it.
This commit is contained in:
Gabriel Brown
2026-08-22 07:38:41 -04:00
parent 8d66247b7c
commit 4910bd659c
2 changed files with 61 additions and 5 deletions
+58 -2
View File
@@ -117,7 +117,7 @@ class RepairResult:
CHECK_ORDER = (
"desktop.hyprland", "desktop.quickshell", "desktop.notifications", "desktop.portals",
"desktop.document-portal",
"desktop.document-portal", "desktop.portal-stability",
"desktop.hyprpaper", "desktop.hypridle", "desktop.hyprlock", "desktop.vicinae", "input.pipewire",
"input.clipboard", "input.wallpaper", "input.capture", "input.ocr", "input.brightness",
"integration.nextcloud", "integration.rustdesk", "integration.kdeconnect", "integration.bluebubbles",
@@ -182,6 +182,7 @@ CHECK_TITLES = {
"desktop.notifications": "Notifications",
"desktop.portals": "Desktop portals",
"desktop.document-portal": "Document portal",
"desktop.portal-stability": "Portal stability",
"desktop.hyprpaper": "Hyprpaper",
"desktop.hypridle": "Hypridle",
"desktop.hyprlock": "Lock screen",
@@ -371,6 +372,61 @@ def check_portals(config: DoctorConfig) -> Check:
return Check("desktop.portals", "desktop-foundation", "Desktop portals", "warning", detail)
def check_portal_stability(config: DoctorConfig) -> Check:
"""Whether the portal backend is crash-looping.
xdg-desktop-portal-hyprland segfaults in its own shutdown path on 1.4.1:
exit() runs the CPortalManager destructor, which tears down
wlr-foreign-toplevel handles and xdg-output proxies after the display has
gone, and dies in wl_map_insert_at. systemd restarts it, so the desktop
keeps working and the only symptom is screen sharing dying mid-call and
the occasional document-portal casualty.
That combination -- harmful but self-repairing -- is exactly what a health
page is for. Sixty crashes in a day left no trace anywhere a person would
look; it took reading coredumpctl to find out, and nobody reads coredumpctl
on a desktop that appears to be working.
Reported rather than repaired. There is nothing to restart that systemd has
not already restarted, and no newer build in the Hyprland COPR to install:
the fix is upstream. Saying so plainly is more use than a button that does
nothing.
"""
result = run_command(
("coredumpctl", "list", "--no-pager", "--since", "24 hours ago",
"/usr/libexec/xdg-desktop-portal-hyprland"),
config,
)
# No coredumps at all makes coredumpctl exit non-zero, which is the good
# case and must not read as a broken probe.
if result.state == "missing":
return Check("desktop.portal-stability", "desktop-foundation", "Portal stability",
"ok", "Crash reporting is not installed, so this cannot be checked.")
if result.state not in {"ok", "failed"}:
return Check("desktop.portal-stability", "desktop-foundation", "Portal stability",
"ok", "Could not read the crash log.")
crashes = sum(
1 for line in result.stdout.splitlines()
if "xdg-desktop-portal-hyprland" in line
)
if crashes == 0:
return Check("desktop.portal-stability", "desktop-foundation", "Portal stability",
"ok", "The portal backend has not crashed today.")
# One or two in a day is the ordinary cost of an upstream bug that only
# bites on shutdown. A dozen means something is driving it, and screen
# sharing will be unreliable.
detail = (
f"The Hyprland portal backend crashed {crashes} time"
f"{'s' if crashes != 1 else ''} in the last day. It restarts itself, so "
"the desktop keeps working, but screen sharing can drop mid-session. "
"This is an upstream fault in its shutdown path, not a setting."
)
return Check("desktop.portal-stability", "desktop-foundation", "Portal stability",
"warning" if crashes >= 5 else "ok", detail)
def check_document_portal(config: DoctorConfig) -> Check:
"""Whether the flatpak document portal is actually mounted.
@@ -719,7 +775,7 @@ def unavailable_versions() -> list[dict[str, str]]:
def collect_checks(config: DoctorConfig) -> list[Check]:
probes: dict[str, Callable[[], Check]] = {
"desktop.hyprland": lambda: check_hyprland(config), "desktop.quickshell": lambda: check_quickshell(config), "desktop.notifications": lambda: check_notifications(config), "desktop.portals": lambda: check_portals(config), "desktop.document-portal": lambda: check_document_portal(config),
"desktop.hyprland": lambda: check_hyprland(config), "desktop.quickshell": lambda: check_quickshell(config), "desktop.notifications": lambda: check_notifications(config), "desktop.portals": lambda: check_portals(config), "desktop.portal-stability": lambda: check_portal_stability(config), "desktop.document-portal": lambda: check_document_portal(config),
"desktop.hyprpaper": lambda: service_check("desktop.hyprpaper", "Hyprpaper", "hyprpaper", config, Action("repair", "Restart Hyprpaper")), "desktop.hypridle": lambda: service_check("desktop.hypridle", "Hypridle", "hypridle", config, Action("repair", "Restart Hypridle")), "desktop.hyprlock": lambda: check_hyprlock(config), "desktop.vicinae": lambda: service_check("desktop.vicinae", "Vicinae", "vicinae", config, Action("repair", "Restart Vicinae")), "input.pipewire": lambda: service_check("input.pipewire", "PipeWire", "pipewire", config),
"input.clipboard": lambda: simple_ipc_check("input.clipboard", "Clipboard", "clipboard", config), "input.wallpaper": lambda: simple_ipc_check("input.wallpaper", "Wallpaper", "wallpaper", config), "input.capture": lambda: simple_ipc_check("input.capture", "Capture", "capture", config), "input.ocr": lambda: executable_check("input.ocr", "OCR", "tesseract", config), "input.brightness": lambda: check_brightness(config),
"integration.nextcloud": lambda: check_nextcloud(config), "integration.rustdesk": lambda: check_rustdesk(config), "integration.kdeconnect": lambda: check_kdeconnect(config), "integration.bluebubbles": lambda: check_bluebubbles(config), "integration.home-assistant": lambda: check_home_assistant(config), "integration.calendar": lambda: check_calendar(config),
+3 -3
View File
@@ -128,7 +128,7 @@ run_doctor() {
/usr/bin/python3 "$doctor" "$@"
}
expected_order=$'desktop.hyprland\ndesktop.quickshell\ndesktop.notifications\ndesktop.portals\ndesktop.document-portal\ndesktop.hyprpaper\ndesktop.hypridle\ndesktop.hyprlock\ndesktop.vicinae\ninput.pipewire\ninput.clipboard\ninput.wallpaper\ninput.capture\ninput.ocr\ninput.brightness\nintegration.nextcloud\nintegration.rustdesk\nintegration.kdeconnect\nintegration.bluebubbles\nintegration.home-assistant\nintegration.calendar\npanama.updates\npanama.runtime-links\npanama.vicinae-commands\npanama.selected-terminal\npanama.selected-launcher\npanama.processes\npanama.caffeine'
expected_order=$'desktop.hyprland\ndesktop.quickshell\ndesktop.notifications\ndesktop.portals\ndesktop.document-portal\ndesktop.portal-stability\ndesktop.hyprpaper\ndesktop.hypridle\ndesktop.hyprlock\ndesktop.vicinae\ninput.pipewire\ninput.clipboard\ninput.wallpaper\ninput.capture\ninput.ocr\ninput.brightness\nintegration.nextcloud\nintegration.rustdesk\nintegration.kdeconnect\nintegration.bluebubbles\nintegration.home-assistant\nintegration.calendar\npanama.updates\npanama.runtime-links\npanama.vicinae-commands\npanama.selected-terminal\npanama.selected-launcher\npanama.processes\npanama.caffeine'
assert_schema_and_redaction() {
local snapshot="$1"
@@ -137,8 +137,8 @@ assert_schema_and_redaction() {
and (.summary.status | IN("healthy", "warning", "error"))
and (.context.session | IN("hyprland", "other"))
and (.context.versions | type == "array")
and ([.checks[].id] | length == 28)
and ([.checks[].id] | unique | length == 28)
and ([.checks[].id] | length == 29)
and ([.checks[].id] | unique | length == 29)
and ([.checks[].status] | all(IN("ok", "warning", "error", "unconfigured")))' \
>/dev/null <<<"$snapshot" || fail "invalid schema: $snapshot"
[[ "$(jq -r '.checks[].id' <<<"$snapshot")" == "$expected_order" ]] \