fix(host/linux): headless gamescope must not inherit a desktop DISPLAY/WAYLAND_DISPLAY

A host (re)started after a desktop login inherits the user manager's
compositor env; a stale WAYLAND_DISPLAY makes headless gamescope 3.16
exit at startup ('Failed to connect to wayland socket') before its
PipeWire node appears. Unset both on the systemd-run transient unit
(UnsetEnvironment=) and the direct spawn (env_remove) — gamescope
exports its own DISPLAY/GAMESCOPE_WAYLAND_DISPLAY to the nested app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:07:41 +02:00
parent 0058f624a2
commit 85513d1697
@@ -1233,6 +1233,10 @@ fn launch_session(client: &str, unit_name: &str, mode: Mode) -> Result<u32> {
let start_unit = || -> Result<()> {
let status = Command::new("systemd-run")
.args(["--user", "--collect", &format!("--unit={unit_name}")])
// Same headless-must-not-attach rule as [`spawn`]: the transient unit inherits the
// user manager env, which can carry a (possibly stale) desktop DISPLAY/WAYLAND_DISPLAY
// that would abort gamescope at startup.
.arg("--property=UnsetEnvironment=DISPLAY WAYLAND_DISPLAY")
.arg("--setenv=BACKEND=headless")
.arg(format!("--setenv=SCREEN_WIDTH={}", mode.width))
.arg(format!("--setenv=SCREEN_HEIGHT={}", mode.height))
@@ -1398,7 +1402,16 @@ fn spawn(w: u32, h: u32, hz: u32, cmd: Option<&str>, log: &std::path::Path) -> R
])
.args(app.split_whitespace())
// Prefer the NVIDIA GL vendor for the nested session (harmless on a pure-NVIDIA box).
.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia")
// A HEADLESS gamescope must never attach to a parent compositor. A host (re)started after
// a desktop login inherits the user manager's DISPLAY/WAYLAND_DISPLAY — and a stale
// WAYLAND_DISPLAY (e.g. a leftover `wayland-kde` in the manager env from a past session)
// makes gamescope 3.16 exit at startup with "Failed to connect to wayland socket" before
// its PipeWire node ever appears (observed 2026-07-14; the boot-started host never saw the
// bug because it predates any login's env import). gamescope exports its own DISPLAY /
// GAMESCOPE_WAYLAND_DISPLAY to the nested app, so the child loses nothing.
.env_remove("DISPLAY")
.env_remove("WAYLAND_DISPLAY");
if let Ok(logf) = std::fs::File::create(log) {
if let Ok(log2) = logf.try_clone() {
cmd.stdout(Stdio::from(logf)).stderr(Stdio::from(log2));