fix(vdisplay/gamescope): the takeover stops a live session's DM on every flavor — the mask never stopped SDDM's relogin
ci / web (push) Successful in 1m7s
ci / docs-site (push) Successful in 2m12s
ci / rust-arm64 (push) Successful in 2m26s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 11s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 8s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 10s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 49s
apple / swift (push) Successful in 4m49s
ci / rust (push) Failing after 4m7s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 18s
android / android (push) Successful in 5m58s
deb / build-publish-client-arm64 (push) Successful in 3m31s
docker / builders-arm64cross (push) Successful in 8s
docker / deploy-docs (push) Successful in 32s
deb / build-publish-host (push) Successful in 4m14s
deb / build-publish (push) Successful in 5m44s
windows-host / package (push) Successful in 11m1s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 21s
arch / build-publish (push) Successful in 14m59s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m34s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m9s
apple / screenshots (push) Successful in 21m19s
ci / web (push) Successful in 1m7s
ci / docs-site (push) Successful in 2m12s
ci / rust-arm64 (push) Successful in 2m26s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 11s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 8s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 10s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 49s
apple / swift (push) Successful in 4m49s
ci / rust (push) Failing after 4m7s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 18s
android / android (push) Successful in 5m58s
deb / build-publish-client-arm64 (push) Successful in 3m31s
docker / builders-arm64cross (push) Successful in 8s
docker / deploy-docs (push) Successful in 32s
deb / build-publish-host (push) Successful in 4m14s
deb / build-publish (push) Successful in 5m44s
windows-host / package (push) Successful in 11m1s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 21s
arch / build-publish (push) Successful in 14m59s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m34s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m9s
apple / screenshots (push) Successful in 21m19s
The managed takeover masked gamescope-session-plus@*.service and trusted the mask to hold SDDM's Relogin=true loop at bay. On images whose sddm session helper execs the session script directly (/etc/sddm/wayland-session gamescope-session-plus steam — f43 bazzite-deck), the relogin never touches the unit: SDDM relogged in ~3x/s for the whole stream, each attempt a full bash --login session start failing against the managed instance. Live on the .41 VM (2026-07-31): 328 forks/s, load 6+, 1481 logind sessions in 8 minutes, the journal flooded past its own rotation — and a 5070 Ti that "could not hold 240fps". A DM that drove a LIVE gaming session is now stopped for the stream's duration on every flavor (the fragile-flavor machinery — lingering guard, pkexec dm-helper, restore via reset-failed+restart, crash-persisted state — already existed and is reused as-is; a pure dm_plan() derives the decision and is unit-tested). The flavors now differ only in the degraded mode when the stop is impossible: SDDM falls back to mask-only (the storm tax, but Steam stays protected — never attach), fragile DMs still fail the takeover into the attach degrade. The mask stays as belt-and-braces for the window before the stop and for images that do route the relogin through the unit. Verified: scripts/xcheck.sh linux clippy (-D warnings, --all-targets) clean; pf-vdisplay suite 125/125 on Ubuntu 26.04. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,11 +83,11 @@ static MANAGED_SESSION: std::sync::Mutex<Option<SessionState>> = std::sync::Mute
|
||||
/// (single-instance), so [`schedule_restore_tv_session`] can restart them when the client disconnects.
|
||||
static STOPPED_AUTOLOGIN: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
/// The display-manager unit we stopped for the takeover on a mask-fragile DM flavor (Nobara's
|
||||
/// `plasmalogin` — see [`dm_survives_masked_unit`]), so the restore brings the box back via
|
||||
/// The display-manager unit we stopped for the takeover (any DM that drove a LIVE gaming session
|
||||
/// is stopped for the stream — see [`dm_plan`]), so the restore brings the box back via
|
||||
/// `reset-failed` + `restart` of the DM instead of a `--user start` of the gamescope unit (which
|
||||
/// cannot work there: without a DM login session there is no seat, so gamescope never gets DRM
|
||||
/// master — live-proven on the Nobara repro VM 2026-07-24).
|
||||
/// cannot work on a mask-fragile flavor: without a DM login session there is no seat, so gamescope
|
||||
/// never gets DRM master — live-proven on the Nobara repro VM 2026-07-24).
|
||||
static STOPPED_DM: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
|
||||
|
||||
/// mtime of the `steamos-session-select` sentinel as of the takeover — the baseline the in-stream
|
||||
@@ -1294,6 +1294,17 @@ fn kill_unit(unit: &str) {
|
||||
/// streaming one — the "stream dies after 30 s–5 min" field reports, diagnosed live on .181
|
||||
/// 2026-07-07). `--runtime` keeps the mask in tmpfs so a reboot clears it even if the host dies
|
||||
/// without restoring (the same semantics as the persisted takeover file).
|
||||
///
|
||||
/// ⚠ The mask only covers the UNIT path — it is NOT what stops the relogin loop itself. On images
|
||||
/// whose SDDM session helper execs the session script directly (`/etc/sddm/wayland-session
|
||||
/// gamescope-session-plus steam`, f43 bazzite-deck — live-diagnosed on the .41 VM 2026-07-31) the
|
||||
/// relogin never touches the unit, so the mask blocks nothing: SDDM relogins ~3×/s, each a full
|
||||
/// `bash --login` session start that fails against the managed instance — 328 forks/s, load 6+,
|
||||
/// 1481 logind sessions in 8 minutes, the journal flooded past its own rotation. The stream itself
|
||||
/// survives, but the storm starves the game and the encoder ("atrocious, unplayable 240fps"). The
|
||||
/// real defense is stopping the DM ([`dm_plan`]); the mask stays as belt-and-braces for the window
|
||||
/// before the stop lands, for images that DO route the relogin through the unit, and as the
|
||||
/// degraded takeover when the stop is impossible.
|
||||
fn mask_unit(unit: &str) {
|
||||
let _ = Command::new("systemctl")
|
||||
.args(["--user", "mask", "--runtime", unit])
|
||||
@@ -1322,19 +1333,58 @@ fn display_manager_unit_under(base: &std::path::Path) -> Option<String> {
|
||||
target.file_name().map(|n| n.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// Does this display manager's autologin loop SURVIVE the gamescope unit being masked? Only SDDM is
|
||||
/// proven to (Bazzite/SteamOS `Relogin=true` — the mask is what stops its relogin from restarting
|
||||
/// the unit mid-stream, diagnosed live on .181 2026-07-07; a failing autologin leaves sddm itself
|
||||
/// running). Nobara's `plasmalogin` (KDE's SDDM successor) is proven FATAL: against a masked unit
|
||||
/// its session Exec fails instantly, `Relogin=true` retries, and `plasmalogin.service` trips
|
||||
/// systemd's start limit within ~1 s — the DM dies and the box is a permanent black screen that
|
||||
/// only a root `reset-failed` + `restart` recovers (live-proven on the Nobara repro VM
|
||||
/// 2026-07-24). Unknown DMs are treated as fragile: the fragile path degrades gracefully, a wrong
|
||||
/// "safe" kills the seat.
|
||||
/// Does this display manager's autologin loop SURVIVE the gamescope unit being masked? This does
|
||||
/// NOT decide whether the DM keeps running — any DM relogin-loops against a killed live gaming
|
||||
/// session, so [`dm_plan`] stops the DM on every flavor — it decides whether masking is safe at
|
||||
/// all, and with it the DEGRADED takeover when the DM can't be stopped (no lingering / no
|
||||
/// privilege):
|
||||
/// * **SDDM** survives (a failing autologin leaves sddm itself running — .181 2026-07-07), so the
|
||||
/// degraded takeover is mask-only: Steam stays protected, and the cost is SDDM's relogin churn
|
||||
/// for the stream's duration — anything from logind/ACL flapping (.181, the audio-flap
|
||||
/// pathology) to a full fork storm on images whose sddm helper bypasses the unit (.41
|
||||
/// 2026-07-31, see [`mask_unit`]).
|
||||
/// * Nobara's `plasmalogin` (KDE's SDDM successor) is proven FATAL: against a masked unit
|
||||
/// its session Exec fails instantly, `Relogin=true` retries, and `plasmalogin.service` trips
|
||||
/// systemd's start limit within ~1 s — the DM dies and the box is a permanent black screen that
|
||||
/// only a root `reset-failed` + `restart` recovers (live-proven on the Nobara repro VM
|
||||
/// 2026-07-24). Unknown DMs are treated as fragile: the fragile path degrades gracefully, a wrong
|
||||
/// "safe" kills the seat.
|
||||
fn dm_survives_masked_unit(dm: &str) -> bool {
|
||||
dm == "sddm.service"
|
||||
}
|
||||
|
||||
/// The takeover's display-manager decision, derived purely from the DM flavor and whether any
|
||||
/// autologin gaming instance is LIVE (unit-tested; the runtime guards — lingering, privilege —
|
||||
/// stay with [`stop_autologin_sessions`]).
|
||||
///
|
||||
/// Killing a live autologin session starts its DM's `Relogin=true` loop, and no flavor tolerates
|
||||
/// that loop well: SDDM's churns logind sessions up to a fork storm ([`mask_unit`]), plasmalogin's
|
||||
/// start-limit-kills the DM. So whenever a DM drove a LIVE gaming session, the DM itself is
|
||||
/// stopped for the stream's duration; the restore ([`do_restore_tv_session`]) brings it back and
|
||||
/// its autologin restores gaming mode. The flavors differ only in masking and in the degraded
|
||||
/// mode ([`dm_survives_masked_unit`]).
|
||||
struct DmPlan {
|
||||
/// Touch nothing at all: a mask-fragile DM with no live gaming instance — killing
|
||||
/// loaded-but-inactive leftovers frees nothing, and stopping the DM would kill the user's
|
||||
/// live desktop for it.
|
||||
skip: bool,
|
||||
/// Mask the units before killing them (safe only where the DM survives a masked unit; also
|
||||
/// the whole of the degraded takeover when the DM can't be stopped).
|
||||
mask: bool,
|
||||
/// Stop the DM for the stream's duration (only a live instance justifies it).
|
||||
stop_dm: bool,
|
||||
}
|
||||
|
||||
/// See [`DmPlan`].
|
||||
fn dm_plan(dm: Option<&str>, any_live: bool) -> DmPlan {
|
||||
let mask = dm.is_none_or(dm_survives_masked_unit);
|
||||
DmPlan {
|
||||
skip: !mask && !any_live,
|
||||
mask,
|
||||
stop_dm: dm.is_some() && any_live,
|
||||
}
|
||||
}
|
||||
|
||||
/// The packaged privileged fallback for the display-manager takeover verbs: a root helper behind
|
||||
/// its own polkit action (`io.unom.punktfunk.dm-helper`, `allow_any` — the mechanism these
|
||||
/// distros use for their own session switcher, e.g. Nobara's `os-session-select`), so the managed
|
||||
@@ -1625,18 +1675,24 @@ fn honor_session_select_switch(dm: String) {
|
||||
/// `punktfunk-gamescope` unit (not a `@`-instance), so it's never matched here. No-op when nothing
|
||||
/// is autologged in (e.g. a box that boots headless).
|
||||
///
|
||||
/// The teardown is DM-flavor-aware ([`dm_survives_masked_unit`]):
|
||||
/// * **SDDM / no DM**: each unit is **masked first** ([`mask_unit`] — SDDM's `Relogin=true` would
|
||||
/// otherwise restart it instantly), then torn down with **SIGKILL** ([`kill_unit`]) to avoid the
|
||||
/// F44 GPU-context leak that the autologin's SIGTERM stop triggers. Matches every loaded
|
||||
/// instance, not just `running` ones — under the SDDM relogin churn the unit flaps through
|
||||
/// `activating`/`failed` between cycles, and an unmasked flapping unit re-enters the fight the
|
||||
/// moment the supervisor restarts it.
|
||||
/// When a display manager drove a LIVE gaming session, it is **stopped for the stream** on every
|
||||
/// flavor ([`dm_plan`]): killing the session otherwise starts the DM's `Relogin=true` loop, which
|
||||
/// at best churns logind sessions/ACLs and at worst is a full fork storm — f43 bazzite-deck's sddm
|
||||
/// helper execs the session script directly, so the masked unit never enters the picture (328
|
||||
/// forks/s, load 6+, live-diagnosed on the .41 VM 2026-07-31 — see [`mask_unit`]). The units
|
||||
/// themselves are torn down with **SIGKILL** ([`kill_unit`]) to avoid the F44 GPU-context leak
|
||||
/// that the autologin's SIGTERM stop triggers. The flavors differ in masking and in the degraded
|
||||
/// mode when the DM can't be stopped (no lingering / no privilege):
|
||||
/// * **SDDM / no DM**: each unit is **masked first** ([`mask_unit`] — belt-and-braces under a
|
||||
/// stopped DM, and the whole defense on images that DO route the relogin through the unit).
|
||||
/// Matches every loaded instance, not just `running` ones — under a relogin churn the unit
|
||||
/// flaps through `activating`/`failed` between cycles, and an unmasked flapping unit re-enters
|
||||
/// the fight the moment the supervisor restarts it. A failed DM stop **degrades to mask-only**
|
||||
/// with a warning, never to attach: the mask still protects Steam, at the storm-tax price.
|
||||
/// * **Mask-fragile DM** (Nobara's `plasmalogin`, unknown DMs): masking start-limit-kills the DM
|
||||
/// itself (permanent black screen), so instead **stop the DM** — no supervisor left to relogin —
|
||||
/// then SIGKILL the units unmasked. Needs privilege (root / an operator polkit rule); without it
|
||||
/// nothing is touched and the error tells the caller to degrade to ATTACH (mirror the box's own
|
||||
/// session) rather than destabilize the seat.
|
||||
/// itself (permanent black screen), so the units are killed unmasked, and a failed DM stop
|
||||
/// **fails the takeover** — the error tells the caller to degrade to ATTACH (mirror the box's
|
||||
/// own session) rather than destabilize the seat.
|
||||
fn stop_autologin_sessions() -> Result<()> {
|
||||
let Ok(out) = Command::new("systemctl")
|
||||
.args([
|
||||
@@ -1667,61 +1723,90 @@ fn stop_autologin_sessions() -> Result<()> {
|
||||
return Ok(()); // nothing autologged in — Steam is already free
|
||||
}
|
||||
let dm = display_manager_unit();
|
||||
let mask_safe = dm.as_deref().is_none_or(dm_survives_masked_unit);
|
||||
if !mask_safe {
|
||||
// Only a LIVE instance holds Steam / justifies touching the DM. A loaded-but-inactive
|
||||
// leftover (the box switched back to the desktop earlier) must not stop the DM — that
|
||||
// would kill the user's live desktop to free nothing.
|
||||
if !listed
|
||||
.iter()
|
||||
.any(|(_, active)| matches!(active.as_str(), "active" | "activating"))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let dm = dm.expect("!is_none_or ⇒ Some");
|
||||
// Only a LIVE instance holds Steam / justifies touching the DM. A loaded-but-inactive
|
||||
// leftover (the box switched back to the desktop earlier) must not stop the DM — that
|
||||
// would kill the user's live desktop to free nothing.
|
||||
let any_live = listed
|
||||
.iter()
|
||||
.any(|(_, active)| matches!(active.as_str(), "active" | "activating"));
|
||||
let plan = dm_plan(dm.as_deref(), any_live);
|
||||
if plan.skip {
|
||||
return Ok(());
|
||||
}
|
||||
if plan.stop_dm {
|
||||
let dm = dm.expect("stop_dm ⇒ Some");
|
||||
// The DM stop ends this user's last login session. If our own lifetime hangs off the user
|
||||
// manager and lingering can't be turned on, that stop kills the host ~10s later — with the
|
||||
// box's display manager down and nobody left to bring it back. Degrading to attach is
|
||||
// strictly better than a black screen that needs a VT to recover.
|
||||
if !ensure_host_survives_dm_stop() {
|
||||
bail!(
|
||||
"stopping {dm} ends this user's last login session, and without lingering logind \
|
||||
would stop the user manager — and this host with it — about 10s later, leaving \
|
||||
the box with no display manager and nothing to restore it; enabling lingering \
|
||||
failed, so the managed takeover is unavailable (run `sudo loginctl enable-linger \
|
||||
$USER` once, as the setup docs ask, then reconnect)"
|
||||
// box's display manager down and nobody left to bring it back. On a mask-fragile flavor,
|
||||
// degrading to attach is strictly better than a black screen that needs a VT to recover;
|
||||
// where masking is safe, mask-only (the storm tax) is strictly better than attach.
|
||||
let dm_stopped = if !ensure_host_survives_dm_stop() {
|
||||
if !plan.mask {
|
||||
bail!(
|
||||
"stopping {dm} ends this user's last login session, and without lingering \
|
||||
logind would stop the user manager — and this host with it — about 10s \
|
||||
later, leaving the box with no display manager and nothing to restore it; \
|
||||
enabling lingering failed, so the managed takeover is unavailable (run \
|
||||
`sudo loginctl enable-linger $USER` once, as the setup docs ask, then \
|
||||
reconnect)"
|
||||
);
|
||||
}
|
||||
tracing::warn!(
|
||||
%dm,
|
||||
"cannot stop the display manager for this stream (lingering could not be \
|
||||
enabled, and without it the DM stop would take this host down ~10s later) — \
|
||||
leaving it running: its autologin Relogin loop will churn logind sessions for \
|
||||
the whole stream, up to a fork storm that starves the game and encoder; run \
|
||||
`sudo loginctl enable-linger $USER` once, as the setup docs ask"
|
||||
);
|
||||
}
|
||||
if !try_stop_display_manager(&dm) {
|
||||
bail!(
|
||||
"the box's gaming session is driven by {dm}, which does not survive a masked \
|
||||
session unit, and stopping it needs privilege — the packaged pf-dm-helper \
|
||||
polkit action is missing or was denied (reinstall the punktfunk package, or \
|
||||
install the display-manager polkit rule from the docs) so the managed takeover \
|
||||
is unavailable"
|
||||
false
|
||||
} else if !try_stop_display_manager(&dm) {
|
||||
if !plan.mask {
|
||||
bail!(
|
||||
"the box's gaming session is driven by {dm}, which does not survive a masked \
|
||||
session unit, and stopping it needs privilege — the packaged pf-dm-helper \
|
||||
polkit action is missing or was denied (reinstall the punktfunk package, or \
|
||||
install the display-manager polkit rule from the docs) so the managed \
|
||||
takeover is unavailable"
|
||||
);
|
||||
}
|
||||
tracing::warn!(
|
||||
%dm,
|
||||
"stopping the display manager for this stream needs privilege — the packaged \
|
||||
pf-dm-helper polkit action is missing or was denied — leaving it running: its \
|
||||
autologin Relogin loop will churn logind sessions for the whole stream, up to a \
|
||||
fork storm that starves the game and encoder (reinstall the punktfunk package, \
|
||||
or install the display-manager polkit rule from the docs)"
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if dm_stopped {
|
||||
tracing::info!(
|
||||
%dm,
|
||||
"freed Steam: stopped the display manager for this stream (its autologin \
|
||||
Relogin loop would otherwise churn against the takeover)"
|
||||
);
|
||||
// Baseline the switch sentinel HERE, not just at a successful launch: setting
|
||||
// STOPPED_DM is what arms the honor gate, so from this instant an unbaselined
|
||||
// sentinel would read as an in-stream "Switch to Desktop" — including the write from
|
||||
// the switch that just brought the box INTO game mode. A successful launch
|
||||
// re-baselines (tighter still).
|
||||
record_session_select_baseline();
|
||||
*STOPPED_DM.lock().unwrap_or_else(|e| e.into_inner()) = Some(dm);
|
||||
}
|
||||
tracing::info!(
|
||||
%dm,
|
||||
"freed Steam: stopped the display manager for this stream (mask-fragile DM flavor)"
|
||||
);
|
||||
// Baseline the switch sentinel HERE, not just at a successful launch: setting STOPPED_DM
|
||||
// is what arms the honor gate, so from this instant an unbaselined sentinel would read as
|
||||
// an in-stream "Switch to Desktop" — including the write from the switch that just brought
|
||||
// the box INTO game mode. A successful launch re-baselines (tighter still).
|
||||
record_session_select_baseline();
|
||||
*STOPPED_DM.lock().unwrap_or_else(|e| e.into_inner()) = Some(dm);
|
||||
}
|
||||
let units: Vec<String> = listed.into_iter().map(|(u, _)| u).collect();
|
||||
let mut stopped = Vec::new();
|
||||
for unit in units {
|
||||
if mask_safe {
|
||||
mask_unit(&unit); // block the SDDM relogin loop from restarting it mid-stream
|
||||
if plan.mask {
|
||||
mask_unit(&unit); // belt-and-braces under a stopped DM; the whole defense otherwise
|
||||
}
|
||||
kill_unit(&unit); // SIGKILL teardown — avoid the F44 GPU-context leak
|
||||
tracing::info!(
|
||||
%unit,
|
||||
masked = mask_safe,
|
||||
masked = plan.mask,
|
||||
"freed Steam: stopped the autologin gaming session for this stream"
|
||||
);
|
||||
stopped.push(unit);
|
||||
@@ -2055,11 +2140,12 @@ fn do_restore_tv_session() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Mask-fragile-DM takeover: the gamescope unit CANNOT be `--user start`ed back — without a DM
|
||||
// login session there is no seat, so gamescope never gets DRM master (unit goes `failed`,
|
||||
// screen stays black — live-proven on the Nobara repro VM). Restore the DM instead
|
||||
// (`reset-failed` clears the relogin start-limit accounting, then `restart`); its autologin
|
||||
// session Exec starts the gamescope unit itself.
|
||||
// DM-stop takeover ([`dm_plan`] — every flavor stops a DM that drove a live gaming session):
|
||||
// restore the DM (`reset-failed` clears any relogin start-limit accounting, then `restart`);
|
||||
// its autologin session Exec brings gaming mode back itself. The unit is NOT `--user start`ed
|
||||
// here: on a mask-fragile flavor that cannot work — without a DM login session there is no
|
||||
// seat, so gamescope never gets DRM master (unit goes `failed`, screen stays black —
|
||||
// live-proven on the Nobara repro VM) — and under SDDM the relogin makes it redundant.
|
||||
if let Some(dm) = dm {
|
||||
let restart = restore_display_manager(&dm);
|
||||
if restart {
|
||||
@@ -2623,8 +2709,9 @@ impl Drop for GamescopeProc {
|
||||
mod tests {
|
||||
use super::{
|
||||
cgroup_is_punktfunk_owned, cgroup_under_user_manager, connected_connector_under,
|
||||
display_manager_unit_under, dm_survives_masked_unit, game_hz, hdr_args, is_steam_launch,
|
||||
missing_flags, nested_wrapper_script, sentinel_advanced, shape_dedicated_command,
|
||||
display_manager_unit_under, dm_plan, dm_survives_masked_unit, game_hz, hdr_args,
|
||||
is_steam_launch, missing_flags, nested_wrapper_script, sentinel_advanced,
|
||||
shape_dedicated_command,
|
||||
};
|
||||
|
||||
/// The HDR spawn flags are what make a nested game render HDR at all — and their absence is
|
||||
@@ -2726,6 +2813,27 @@ mod tests {
|
||||
std::fs::remove_dir_all(&base).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dm_plan_stops_any_dm_that_drove_a_live_session() {
|
||||
// SDDM, live gaming session: mask (belt-and-braces) AND stop the DM — the mask alone
|
||||
// does not stop the relogin loop on images whose sddm helper execs the session script
|
||||
// directly, bypassing the unit (fork storm, .41 VM 2026-07-31).
|
||||
let p = dm_plan(Some("sddm.service"), true);
|
||||
assert!(!p.skip && p.mask && p.stop_dm);
|
||||
// SDDM, only inactive leftovers: nothing live justifies touching the DM — mask+kill only.
|
||||
let p = dm_plan(Some("sddm.service"), false);
|
||||
assert!(!p.skip && p.mask && !p.stop_dm);
|
||||
// Mask-fragile flavor, live: stop the DM, never mask (masking start-limit-kills the DM).
|
||||
let p = dm_plan(Some("plasmalogin.service"), true);
|
||||
assert!(!p.skip && !p.mask && p.stop_dm);
|
||||
// Mask-fragile flavor, nothing live: hands off entirely — stopping the DM here would
|
||||
// kill the user's live desktop to free nothing.
|
||||
assert!(dm_plan(Some("plasmalogin.service"), false).skip);
|
||||
// No DM at all (getty autologin): mask+kill, nothing to stop.
|
||||
let p = dm_plan(None, true);
|
||||
assert!(!p.skip && p.mask && !p.stop_dm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connector_status_scan() {
|
||||
let base = std::env::temp_dir().join(format!("pf-drm-scan-{}", std::process::id()));
|
||||
|
||||
Reference in New Issue
Block a user