fix(headless/kde): start Xwayland + detect its display so X11 apps work
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 31s
apple / swift (push) Successful in 1m19s
ci / rust (push) Successful in 2m4s
ci / bench (push) Successful in 1m41s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 2m21s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m49s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m27s

X11/Electron apps (Discord — "Missing X Server or $DISPLAY", Steam, many launchers) failed in the
headless KWin session: `kwin_wayland --virtual` starts NO X server unless asked, and even with one
KWin reserves the X11 display + starts Xwayland *on demand* (no Xwayland process or "Using public
X11 display" log line until the first client connects) — so the old detection (pgrep the Xwayland
process) found nothing and never exported DISPLAY. Two fixes: pass `--xwayland`, and detect the
display from the reserved /tmp/.X11-unix/X<N> socket (with the log + process checks as fallbacks).
Verified live on the Fedora 44 KDE box: DISPLAY=:0 lands in plasmashell + the activation env and
xdpyinfo responds, so menu-launched X11 apps open a display.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 23:17:56 +00:00
parent 5c1aa453c1
commit d78bbdffe2
+18 -3
View File
@@ -48,8 +48,12 @@ else
fi fi
# kwin to its own log (so its EGL/GPU-init errors are captured, not lost to the terminal). # kwin to its own log (so its EGL/GPU-init errors are captured, not lost to the terminal).
# `--xwayland`: enable Xwayland so X11 apps (Discord/Electron, Steam, many launchers) work. Without
# it `kwin_wayland --virtual` brings up NO X server at all (no display reserved), and those apps die
# with "Missing X Server or $DISPLAY". KWin starts Xwayland on demand but reserves + logs the X11
# display up front, which the detection below reads.
KWIN_LOG="${TMPDIR:-/tmp}/punktfunk-kwin.log" KWIN_LOG="${TMPDIR:-/tmp}/punktfunk-kwin.log"
kwin_wayland --virtual --width "$W" --height "$H" --no-lockscreen \ kwin_wayland --virtual --xwayland --width "$W" --height "$H" --no-lockscreen \
--socket "$WAYLAND_DISPLAY" >"$KWIN_LOG" 2>&1 & --socket "$WAYLAND_DISPLAY" >"$KWIN_LOG" 2>&1 &
KWIN_PID=$! KWIN_PID=$!
@@ -78,13 +82,24 @@ echo "KWin ready."
# is running — KWin sets DISPLAY only for its own children, not for apps launched via the # is running — KWin sets DISPLAY only for its own children, not for apps launched via the
# plasma menu / D-Bus activation. KWin brings Xwayland up a moment after itself; poll for it. # plasma menu / D-Bus activation. KWin brings Xwayland up a moment after itself; poll for it.
DISPLAY_NUM="" DISPLAY_NUM=""
for _ in $(seq 1 20); do for _ in $(seq 1 40); do
# `|| true`: under `set -euo pipefail`, pgrep/grep exit non-zero when Xwayland isn't up yet # `|| true`: under `set -euo pipefail`, grep/pgrep exit non-zero when nothing's there yet
# (common a few seconds into a systemd-launched boot), which would abort the WHOLE script — # (common a few seconds into a systemd-launched boot), which would abort the WHOLE script —
# killing KWin — on the first iteration instead of retrying. Tolerate it so the loop polls, # killing KWin — on the first iteration instead of retrying. Tolerate it so the loop polls,
# and so a session with no Xwayland at all still proceeds (DISPLAY just stays unset → warn). # and so a session with no Xwayland at all still proceeds (DISPLAY just stays unset → warn).
# Primary: KWin reserves + logs the X11 display ("Using public X11 display :N") up front, even
# with Xwayland-on-demand (no Xwayland *process* until the first X client connects) — so the
# old pgrep-the-process check found nothing and never set DISPLAY. Read the log; fall back to a
# live Xwayland process for older KWin.
d=$(grep -oE 'Using public X11 display :[0-9]+' "$KWIN_LOG" 2>/dev/null | grep -oE '[0-9]+' | head -1 || true)
if [[ -n "$d" ]]; then DISPLAY_NUM="$d"; break; fi
d=$(pgrep -a Xwayland 2>/dev/null | grep -oE ' :[0-9]+' | tr -d ' :' | head -1 || true) d=$(pgrep -a Xwayland 2>/dev/null | grep -oE ' :[0-9]+' | tr -d ' :' | head -1 || true)
if [[ -n "$d" && -S "/tmp/.X11-unix/X$d" ]]; then DISPLAY_NUM="$d"; break; fi if [[ -n "$d" && -S "/tmp/.X11-unix/X$d" ]]; then DISPLAY_NUM="$d"; break; fi
# Most reliable here: KWin --xwayland reserves the /tmp/.X11-unix/X<N> socket up front (Xwayland
# starts on demand on first connect) and on this KWin neither logs the display nor runs a process
# until then. The socket IS the signal — take it. (Fresh boot has a tmpfs /tmp, so no stale one.)
s=$(ls /tmp/.X11-unix/X[0-9]* 2>/dev/null | head -1 || true)
if [[ -n "$s" && -S "$s" ]]; then DISPLAY_NUM="${s##*/X}"; break; fi
sleep 0.25 sleep 0.25
done done
if [[ -n "$DISPLAY_NUM" ]]; then if [[ -n "$DISPLAY_NUM" ]]; then