fix(host/linux): survive and self-heal dead gamescope sessions on headless SteamOS boxes

Three fixes for the "most connects simply fail" trap on a SteamOS host
with no physical display (VM testbox, panel-less mini PC):

- restore path: skip the restore-to-physical-panel when no DRM connector
  reports a connected display — removing the headless drop-in and
  restarting gamescope-session.target on such a box just crash-loops
  gamescope and strands every later connect on "no usable compositor".
  Keep the headless session (and the takeover state) instead.
- connect path: when no live graphical session exists but the MANAGED
  gamescope infra is present (SteamOS gamescope-session / Bazzite
  session-plus), route to the gamescope takeover — which rebuilds the
  session at the client's mode — instead of failing the connect.
- error delivery: a session-setup failure used to just drop the QUIC
  connection, reaching the client as "control stream finished mid-frame"
  (indistinguishable from transport trouble). Close with a typed
  setup-failed code (0x68, RejectedSetupFailed = -29) carrying the error
  text, and give the client handshake/pairing error paths a short grace
  for the CONNECTION_CLOSE to beat the stream error it caused.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 09:50:30 +02:00
co-authored by Claude Fable 5
parent 87e7c82cbc
commit 2562663fc6
11 changed files with 177 additions and 13 deletions
+17 -1
View File
@@ -92,8 +92,24 @@ pub(super) fn resolve_compositor(
let available = crate::vdisplay::available();
let chosen = match pick_compositor(pref, &available, detected) {
Some(c) => c,
// No live session, but the MANAGED gamescope infra exists (SteamOS's
// `gamescope-session`, Bazzite's `gamescope-session-plus`): route to the gamescope
// backend anyway — its managed path stands the session up from nothing at the
// client's mode (drop-in takeover / session relaunch), so a dead gaming session
// self-heals on the next connect instead of bouncing every client until someone
// restarts it by hand. (The trap that motivated this: a headless SteamOS box whose
// gamescope died — every connect failed "no usable compositor" even though the
// takeover could rebuild it.) Not under an operator pin: an explicit
// `PUNKTFUNK_COMPOSITOR` keeps its exact, hand-configured meaning.
None if !overridden && crate::vdisplay::managed_session_available() => {
tracing::info!(
"no live graphical session — managed gamescope infra present; routing to \
the managed takeover to revive the session"
);
Compositor::Gamescope
}
None => {
// No live session — the state a compositor crash leaves behind (gnome-shell
// The state a compositor crash leaves behind (gnome-shell
// SIGSEGV → GDM greeter, whose auto-login is once-per-boot). If the operator
// configured a recovery hook, fire it (debounced) and tell the client to retry:
// its next knock lands in the recovered desktop.