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:
@@ -86,10 +86,22 @@ impl NativeClient {
|
||||
// A typed application close from the host (pairing not armed / armed for a
|
||||
// different device / rate-limited / version mismatch) beats the generic
|
||||
// transport error the aborted exchange produced — it is the actual answer.
|
||||
Err(e) => Err(match reject_from_close(&conn) {
|
||||
Some(r) => PunktfunkError::Rejected(r),
|
||||
None => e,
|
||||
}),
|
||||
// Same close-vs-stream-error race as the connect handshake: give the
|
||||
// host's CONNECTION_CLOSE a short grace to be processed before deciding
|
||||
// the error was plain transport trouble.
|
||||
Err(e) => {
|
||||
if conn.close_reason().is_none() {
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(300),
|
||||
conn.closed(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(match reject_from_close(&conn) {
|
||||
Some(r) => PunktfunkError::Rejected(r),
|
||||
None => e,
|
||||
})
|
||||
}
|
||||
ok => ok,
|
||||
};
|
||||
// Always tell the host we're done so it never blocks at its read — code 0 on
|
||||
|
||||
@@ -240,9 +240,22 @@ pub(super) async fn connect_and_handshake(args: &WorkerArgs) -> Result<Handshake
|
||||
negotiated,
|
||||
host_caps,
|
||||
}),
|
||||
Err(e) => Err(match reject_from_close(&conn) {
|
||||
Some(r) => PunktfunkError::Rejected(r),
|
||||
None => e,
|
||||
}),
|
||||
Err(e) => {
|
||||
// The host's typed close can land a beat AFTER the stream error it caused: the
|
||||
// stream reset/FIN and the CONNECTION_CLOSE are in flight together, and quinn can
|
||||
// hand the reader its mid-frame EOF before it processes the close. Give the close a
|
||||
// short grace to arrive so a host-side setup failure renders as its real reason
|
||||
// ("the host could not start the stream session") instead of "control stream
|
||||
// finished mid-frame". No-op when the connection already closed (or never will —
|
||||
// bounded by the timeout).
|
||||
if conn.close_reason().is_none() {
|
||||
let _ = tokio::time::timeout(std::time::Duration::from_millis(300), conn.closed())
|
||||
.await;
|
||||
}
|
||||
Err(match reject_from_close(&conn) {
|
||||
Some(r) => PunktfunkError::Rejected(r),
|
||||
None => e,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user