diff --git a/config/dot/quickshell/scripts/panama-doctor b/config/dot/quickshell/scripts/panama-doctor index 542b6a3..8e57770 100755 --- a/config/dot/quickshell/scripts/panama-doctor +++ b/config/dot/quickshell/scripts/panama-doctor @@ -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), diff --git a/tests/quickshell/panama-doctor-contract b/tests/quickshell/panama-doctor-contract index 9e35c3c..76d29bc 100755 --- a/tests/quickshell/panama-doctor-contract +++ b/tests/quickshell/panama-doctor-contract @@ -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" ]] \