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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ pub enum PunktfunkStatus {
|
||||
RejectedSuperseded = -26,
|
||||
RejectedWireVersion = -27,
|
||||
RejectedBusy = -28,
|
||||
RejectedSetupFailed = -29,
|
||||
Panic = -99,
|
||||
}
|
||||
|
||||
@@ -89,6 +90,7 @@ impl PunktfunkError {
|
||||
R::Superseded => PunktfunkStatus::RejectedSuperseded,
|
||||
R::WireVersionMismatch => PunktfunkStatus::RejectedWireVersion,
|
||||
R::Busy => PunktfunkStatus::RejectedBusy,
|
||||
R::SetupFailed => PunktfunkStatus::RejectedSetupFailed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,12 @@ pub const PAIR_APPROVAL_TIMEOUT_CLOSE_CODE: u32 = 0x65;
|
||||
pub const PAIR_SUPERSEDED_CLOSE_CODE: u32 = 0x66;
|
||||
/// The client's wire (protocol) version does not match the host's — one side needs updating.
|
||||
pub const WIRE_VERSION_CLOSE_CODE: u32 = 0x67;
|
||||
/// The host admitted the connection but could not stand the stream session up (compositor /
|
||||
/// capture / encoder setup failed host-side). The close reason bytes carry the specific error
|
||||
/// text for logs/diagnostics; clients render a stable "host-side failure" sentence. Before this
|
||||
/// code, a setup failure reached the client as a bare dropped connection ("control stream
|
||||
/// finished mid-frame") — indistinguishable from transport trouble.
|
||||
pub const SETUP_FAILED_CLOSE_CODE: u32 = 0x68;
|
||||
|
||||
/// Why a host turned a connection away, decoded from the QUIC application close code — the
|
||||
/// client-side view of [`PAIR_NOT_ARMED_CLOSE_CODE`]..[`WIRE_VERSION_CLOSE_CODE`] plus
|
||||
@@ -59,6 +65,9 @@ pub enum RejectReason {
|
||||
WireVersionMismatch,
|
||||
/// The host refused admission because a conflicting session is live.
|
||||
Busy,
|
||||
/// The host admitted the connection but failed to start the stream session (host-side
|
||||
/// setup error — the host log has the specific cause).
|
||||
SetupFailed,
|
||||
}
|
||||
|
||||
impl RejectReason {
|
||||
@@ -75,6 +84,7 @@ impl RejectReason {
|
||||
PAIR_SUPERSEDED_CLOSE_CODE => Self::Superseded,
|
||||
WIRE_VERSION_CLOSE_CODE => Self::WireVersionMismatch,
|
||||
REJECT_BUSY_CLOSE_CODE => Self::Busy,
|
||||
SETUP_FAILED_CLOSE_CODE => Self::SetupFailed,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -91,6 +101,7 @@ impl RejectReason {
|
||||
Self::Superseded => PAIR_SUPERSEDED_CLOSE_CODE,
|
||||
Self::WireVersionMismatch => WIRE_VERSION_CLOSE_CODE,
|
||||
Self::Busy => REJECT_BUSY_CLOSE_CODE,
|
||||
Self::SetupFailed => SETUP_FAILED_CLOSE_CODE,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +118,7 @@ impl RejectReason {
|
||||
Self::Superseded => "superseded",
|
||||
Self::WireVersionMismatch => "wire-version",
|
||||
Self::Busy => "busy",
|
||||
Self::SetupFailed => "setup-failed",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,6 +137,7 @@ impl std::fmt::Display for RejectReason {
|
||||
Self::Superseded => "a newer request from this device replaced this one",
|
||||
Self::WireVersionMismatch => "client and host versions do not match",
|
||||
Self::Busy => "the host is busy with another session",
|
||||
Self::SetupFailed => "the host could not start the stream session",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user