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:
@@ -423,6 +423,10 @@ pub(crate) async fn serve(
|
||||
// permit's lifetime: it's released while a knock is parked for delegated approval and
|
||||
// re-acquired on approval, so the hold is no longer a simple closure-scoped binding.
|
||||
let sem_session = sem.clone();
|
||||
// Kept for the error path below: `serve_session` consumes `conn`, but a setup failure
|
||||
// must still close the connection with a typed reason (quinn connections are cheap
|
||||
// Arc-handle clones).
|
||||
let conn_err = conn.clone();
|
||||
sessions.spawn(async move {
|
||||
match serve_session(
|
||||
conn,
|
||||
@@ -445,7 +449,23 @@ pub(crate) async fn serve(
|
||||
"closed before the control handshake (reachability probe)"
|
||||
),
|
||||
Err(e) => {
|
||||
tracing::warn!(%peer, error = %format!("{e:#}"), "session ended with error")
|
||||
// Make the failure legible to the client (the [`close_rejected`] discipline,
|
||||
// extended to EVERY session error): a setup failure that just drops the
|
||||
// connection reaches the client as a bare close mid-control-frame ("control
|
||||
// stream finished mid-frame") — indistinguishable from transport trouble.
|
||||
// Close with the typed setup-failed code, carrying the error text in the
|
||||
// reason bytes for client-side logs. When a gate already closed with its own
|
||||
// typed code, or the peer closed first, this close is a no-op (first wins).
|
||||
let detail = format!("{e:#}");
|
||||
let mut cut = detail.len().min(256);
|
||||
while !detail.is_char_boundary(cut) {
|
||||
cut -= 1;
|
||||
}
|
||||
conn_err.close(
|
||||
punktfunk_core::reject::SETUP_FAILED_CLOSE_CODE.into(),
|
||||
detail[..cut].as_bytes(),
|
||||
);
|
||||
tracing::warn!(%peer, error = %detail, "session ended with error")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user