fix(vdisplay/gamescope): DM-flavor-aware session takeover — stop masking Nobara's plasmalogin to death
Field report (Nobara 43 HTPC): switching the host to Steam Game Mode mid-stream permanently black-screened the box. Live-proven root cause on a Nobara repro VM: our takeover masks the box's gamescope-session-plus unit, plasmalogin's Relogin=true then fails its session Exec repeatedly and trips systemd's start limit within ~1 s — the display manager dies, and our restore verb (unmask + user start) cannot bring a seatless gamescope back. Only 'reset-failed + restart' of the DM recovers. The takeover is now display-manager-flavor-aware: - SDDM (Bazzite/SteamOS) keeps the proven mask+SIGKILL path unchanged. - plasmalogin/unknown DMs never mask: with privilege (root or an operator polkit rule scoped to the DM unit — documented) the host stops the DM for the stream and restores it with reset-failed + restart (PUNKTFUNK_RECOVER_SESSION_CMD as fallback), recorded in the persisted takeover state so a host crash still restores; without privilege the managed takeover degrades to ATTACH and mirrors the box's live Game Mode instead of destabilizing the seat. Both legs of the privileged cycle live-verified on the repro VM (headless managed session works with zero login sessions; render nodes are 0666). A loaded-but-inactive leftover instance never triggers the DM stop. Companion fixes from the same triage: - ensure_box_gamescope_mode gains the attach-only rebuild-probe guard both managed paths already had (stale post-capture-loss detection restarted the box's unit), and no longer re-modes a box that drives a physical display — attach mirrors on-glass; re-mode is the headless-box model. - Capture-loss rebuilds targeting gamescope get a 100 s budget: the 40 s budget expired inside the first 45 s Steam-cold-start launch attempt, a guaranteed single-shot failure. - A PUNKTFUNK_COMPOSITOR pin now WARNs once per capture loss when the live session no longer matches it (the pin disables session-following — the reporter's original stream-death trigger). - A managed session that took nothing over (client gamescope pin beside a live desktop) is stopped on disconnect instead of being orphaned forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1940,6 +1940,15 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
||||
// connected, frozen on the last frame, and the stream resumes when the new output
|
||||
// appears — no reconnect.
|
||||
const REBUILD_BUDGET: std::time::Duration = std::time::Duration::from_secs(40);
|
||||
// A managed/attach gamescope (re)launch legitimately takes up to 45 s — the Steam
|
||||
// Big Picture cold start that `launch_session`/`ensure_box_gamescope_mode` poll
|
||||
// for — so the 40 s budget used to expire INSIDE the first attempt (a single-shot
|
||||
// failure ending the session even when a second, warm attempt would have
|
||||
// succeeded). Give gamescope-targeted rebuilds room for two full launch attempts;
|
||||
// desktop compositors keep the tighter budget. Checked per iteration because the
|
||||
// loop retargets `compositor` as re-detection follows the box.
|
||||
const GAMESCOPE_REBUILD_BUDGET: std::time::Duration =
|
||||
std::time::Duration::from_secs(100);
|
||||
// Attach-only holdoff: for the first seconds after a capture loss the session
|
||||
// detection can be STALE (the new session isn't up yet), and a rebuild acting on
|
||||
// a stale "Gaming" answer restarts gamescope-session.target — which on SteamOS
|
||||
@@ -1948,7 +1957,24 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
||||
// scope: attach to live outputs only, never stop/relaunch/take over sessions.
|
||||
const PROBE_HOLDOFF: std::time::Duration = std::time::Duration::from_secs(4);
|
||||
let loss_at = std::time::Instant::now();
|
||||
let rebuild_deadline = loss_at + REBUILD_BUDGET;
|
||||
// An explicit PUNKTFUNK_COMPOSITOR pin disables the re-detection below — the
|
||||
// stream cannot follow a session switch. When the live session no longer matches
|
||||
// the pin, say so loudly ONCE per loss: this rebuild can only retry the pinned
|
||||
// backend and will die at the budget (the "mid-stream switch to game mode kills
|
||||
// the stream" field reports all traced back to a stale pin).
|
||||
if pf_host_config::config().compositor.is_some() {
|
||||
let active = crate::vdisplay::detect_active_session();
|
||||
if crate::vdisplay::compositor_for_kind(active.kind) != Some(compositor) {
|
||||
tracing::warn!(
|
||||
pinned = compositor.id(),
|
||||
live = ?active.kind,
|
||||
"capture lost while PUNKTFUNK_COMPOSITOR pins the backend and the \
|
||||
live session no longer matches it — the pin disables \
|
||||
session-following, so this rebuild can only retry the pinned \
|
||||
backend; remove the pin to let the stream follow session switches"
|
||||
);
|
||||
}
|
||||
}
|
||||
let (new_cap, new_enc, new_frame, new_interval, new_node_id, new_display_gen) = loop {
|
||||
// Follow the active session unless an explicit PUNKTFUNK_COMPOSITOR pin forbids
|
||||
// retargeting (then we stick to the pinned backend and just rebuild it).
|
||||
@@ -2021,8 +2047,13 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
||||
) {
|
||||
Ok(p) => break p,
|
||||
Err(e2) => {
|
||||
let budget = if compositor == crate::vdisplay::Compositor::Gamescope {
|
||||
GAMESCOPE_REBUILD_BUDGET
|
||||
} else {
|
||||
REBUILD_BUDGET
|
||||
};
|
||||
if stop.load(Ordering::SeqCst)
|
||||
|| std::time::Instant::now() >= rebuild_deadline
|
||||
|| std::time::Instant::now() >= loss_at + budget
|
||||
{
|
||||
return Err(e2)
|
||||
.context("capture lost — no compositor came up within the rebuild budget");
|
||||
|
||||
Reference in New Issue
Block a user