From 58b27acbd51da64e10ce2122104c745be1bb7ec7 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Tue, 21 Jul 2026 11:06:55 +0200 Subject: [PATCH] fix(vdisplay/session): scrub the dead desktop's WAYLAND_DISPLAY from the user-manager env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit settle_desktop_portal pushes the live desktop's WAYLAND_DISPLAY into the systemd --user manager (import-environment) so a re-activated portal inherits the right session — but the import PERSISTS after that desktop dies. Every later user unit then inherits the stale socket, including gamescope-session.target: gamescope sees WAYLAND_DISPLAY, runs NESTED, and aborts with "Failed to connect to wayland socket: wayland-0" — observed live on a Deck, where Game Mode could not start at all (autologin crash-looped) until the var was unset by hand. Two-layer fix, mirroring the protection launch_session already gives its transient unit: - observe_session_instance: when a desktop compositor instance goes away, unset WAYLAND_DISPLAY/DISPLAY from the manager env (a desktop bounce is harmless — the next portal settle re-imports). - write_steamos_dropin: UnsetEnvironment=DISPLAY WAYLAND_DISPLAY on the takeover drop-in, unit-scoped belt-and-suspenders for host-initiated restarts. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/vdisplay/linux/gamescope.rs | 7 +++++- crates/pf-vdisplay/src/vdisplay/session.rs | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/pf-vdisplay/src/vdisplay/linux/gamescope.rs b/crates/pf-vdisplay/src/vdisplay/linux/gamescope.rs index 38279404..b8a3ec19 100644 --- a/crates/pf-vdisplay/src/vdisplay/linux/gamescope.rs +++ b/crates/pf-vdisplay/src/vdisplay/linux/gamescope.rs @@ -630,12 +630,17 @@ fn write_steamos_dropin(shim_dir: &std::path::Path, mode: Mode) -> Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).with_context(|| format!("mkdir {}", parent.display()))?; } + // UnsetEnvironment: the same headless-must-not-attach armor `launch_session` gives its + // transient unit — the manager env can carry a stale desktop DISPLAY/WAYLAND_DISPLAY (from a + // portal settle), and gamescope would abort trying to attach to it instead of becoming the + // display server. Unit-scoped belt-and-suspenders on top of the observe_session_instance scrub. let body = format!( "[Service]\n\ Environment=PATH={shim}:/usr/bin:/bin:/usr/local/bin\n\ Environment=PF_W={w}\n\ Environment=PF_H={h}\n\ - Environment=PF_HZ={hz}\n", + Environment=PF_HZ={hz}\n\ + UnsetEnvironment=DISPLAY WAYLAND_DISPLAY\n", shim = shim_dir.display(), w = mode.width, h = mode.height, diff --git a/crates/pf-vdisplay/src/vdisplay/session.rs b/crates/pf-vdisplay/src/vdisplay/session.rs index 1253e63c..3a27b5b4 100644 --- a/crates/pf-vdisplay/src/vdisplay/session.rs +++ b/crates/pf-vdisplay/src/vdisplay/session.rs @@ -57,6 +57,13 @@ pub fn observe_session_instance(active: &ActiveSession) { if let Some(old) = compositor_for_kind(prev.0) { registry::invalidate_backend(old.id()); } + // The dead desktop's socket vars may still sit in the systemd --user manager env + // ([`settle_desktop_portal`]'s import-environment) — scrub them NOW, or the next + // `gamescope-session.target` start inherits a stale WAYLAND_DISPLAY and gamescope + // runs NESTED against the dead desktop socket instead of becoming the display + // server ("Failed to connect to wayland socket: wayland-0" — kept a Deck's Game + // Mode from starting at all, observed live 2026-07-21). + scrub_desktop_manager_env(); } let epoch = bump_session_epoch(); tracing::info!( @@ -70,6 +77,23 @@ pub fn observe_session_instance(active: &ActiveSession) { *last = Some(cur); } +/// Counterpart to [`settle_desktop_portal`]'s `import-environment`: drop the desktop session's +/// socket vars from the systemd `--user` manager env once that desktop instance is GONE. They +/// persist in the manager otherwise, and every later user unit inherits them — including +/// `gamescope-session.target`, whose gamescope then aborts trying to attach to the dead desktop +/// socket. Best-effort; the D-Bus activation env has no unset op, but gamescope-session is +/// systemd-started, so the manager scrub is the one that matters. (A desktop restart re-imports +/// via the next [`settle_desktop_portal`], so scrubbing on a bounce is harmless.) +#[cfg(target_os = "linux")] +fn scrub_desktop_manager_env() { + let _ = std::process::Command::new("systemctl") + .args(["--user", "unset-environment", "WAYLAND_DISPLAY", "DISPLAY"]) + .status(); +} + +#[cfg(not(target_os = "linux"))] +fn scrub_desktop_manager_env() {} + /// Is `kind` a **desktop** compositor (KWin / Mutter / wlroots) — one whose kept PipeWire outputs die /// with the compositor instance, so the session epoch tracks it? `Gaming` (gamescope) and `None` are /// not (gamescope spawns are independent nested sessions — see [`observe_session_instance`]).