feat(host): hold a logind sleep/idle inhibitor while clients stream
ci / docs-site (push) Successful in 56s
ci / web (push) Successful in 1m0s
android / android (push) Failing after 1m6s
apple / swift (push) Successful in 1m19s
decky / build-publish (push) Successful in 48s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 52s
ci / bench (push) Successful in 8m13s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m40s
deb / build-publish (push) Successful in 11m0s
release / apple (push) Successful in 9m50s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10m19s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8m57s
docker / deploy-docs (push) Successful in 35s
deb / build-publish-host (push) Successful in 14m3s
apple / screenshots (push) Successful in 6m27s
arch / build-publish (push) Successful in 17m49s
windows-host / package (push) Successful in 18m15s
flatpak / build-publish (push) Failing after 8m25s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m24s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m37s
ci / rust (push) Successful in 25m51s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m39s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m52s
ci / docs-site (push) Successful in 56s
ci / web (push) Successful in 1m0s
android / android (push) Failing after 1m6s
apple / swift (push) Successful in 1m19s
decky / build-publish (push) Successful in 48s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 52s
ci / bench (push) Successful in 8m13s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m40s
deb / build-publish (push) Successful in 11m0s
release / apple (push) Successful in 9m50s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10m19s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8m57s
docker / deploy-docs (push) Successful in 35s
deb / build-publish-host (push) Successful in 14m3s
apple / screenshots (push) Successful in 6m27s
arch / build-publish (push) Successful in 17m49s
windows-host / package (push) Successful in 18m15s
flatpak / build-publish (push) Failing after 8m25s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m24s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m37s
ci / rust (push) Successful in 25m51s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m39s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m52s
Remote INPUT resets the compositor's idle timers, but a passive (video-only) viewer sends none — observed live: a SteamOS Game-Mode host s2idled mid-day and dropped off the network (in a VM with GPU passthrough it never woke again). Refcounted across planes: the native LiveSessionGuard and the GameStream video worker each hold a share; first hold takes the logind sleep:idle block inhibitor on a dedicated plain thread (zbus blocking must not run on a tokio worker), last drop releases. Best-effort: no logind → warn once and stream on. No-op off Linux (macOS/Windows manage their own assertions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,9 @@ pub fn start(
|
|||||||
// Same scheduling posture as the native path's capture/encode thread (Linux nice -10 /
|
// Same scheduling posture as the native path's capture/encode thread (Linux nice -10 /
|
||||||
// Windows HIGHEST + session tuning) — GameStream previously ran unboosted on Linux.
|
// Windows HIGHEST + session tuning) — GameStream previously ran unboosted on Linux.
|
||||||
crate::native::boost_thread_priority(true);
|
crate::native::boost_thread_priority(true);
|
||||||
|
// A GameStream viewer may be video-only too — hold the suspend/idle inhibitor for
|
||||||
|
// this stream's lifetime (plane parity with the native LiveSessionGuard).
|
||||||
|
let _sleep = crate::sleep_inhibit::hold();
|
||||||
tracing::info!(?cfg, "video stream starting");
|
tracing::info!(?cfg, "video stream starting");
|
||||||
// Lifecycle events + the script-facing marker file, plane parity with the native loop
|
// Lifecycle events + the script-facing marker file, plane parity with the native loop
|
||||||
// (RFC §4): `announce` emits `stream.started`/`stream.stopped` and holds the marker for
|
// (RFC §4): `announce` emits `stream.started`/`stream.stopped` and holds the marker for
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ mod send_pacing;
|
|||||||
mod service;
|
mod service;
|
||||||
mod session_plan;
|
mod session_plan;
|
||||||
mod session_status;
|
mod session_status;
|
||||||
|
mod sleep_inhibit;
|
||||||
mod spike;
|
mod spike;
|
||||||
mod stats_recorder;
|
mod stats_recorder;
|
||||||
// The plugin store: signed catalogs, tiered trust, and install/uninstall jobs that run through the
|
// The plugin store: signed catalogs, tiered trust, and install/uninstall jobs that run through the
|
||||||
|
|||||||
@@ -114,12 +114,18 @@ pub fn register(
|
|||||||
session: session_ref(&session),
|
session: session_ref(&session),
|
||||||
});
|
});
|
||||||
registry().lock().unwrap().push(session);
|
registry().lock().unwrap().push(session);
|
||||||
LiveSessionGuard { id }
|
LiveSessionGuard {
|
||||||
|
id,
|
||||||
|
_sleep: crate::sleep_inhibit::hold(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes its session from the registry when dropped (any scope exit of the native video loop).
|
/// Removes its session from the registry when dropped (any scope exit of the native video loop).
|
||||||
pub struct LiveSessionGuard {
|
pub struct LiveSessionGuard {
|
||||||
id: u64,
|
id: u64,
|
||||||
|
/// While any native session lives, the box must not auto-suspend under a passive viewer
|
||||||
|
/// ([`crate::sleep_inhibit`]) — refcounted, released with the guard.
|
||||||
|
_sleep: crate::sleep_inhibit::StreamHold,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for LiveSessionGuard {
|
impl Drop for LiveSessionGuard {
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
//! Session-scoped suspend/idle inhibition: while at least one client is streaming, the host
|
||||||
|
//! holds a logind `sleep:idle` BLOCK inhibitor so the box doesn't auto-suspend out from under a
|
||||||
|
//! passive viewer. Remote INPUT resets the compositor's idle timers, but a video-only viewer
|
||||||
|
//! sends none — observed live on a SteamOS Game-Mode host, which s2idled mid-stream-day and
|
||||||
|
//! dropped off the network (and, in a VM with GPU passthrough, never woke again). Refcounted
|
||||||
|
//! across planes (native sessions + GameStream media): the first hold acquires, the last drop
|
||||||
|
//! releases. Best-effort — no logind (containers, non-systemd boxes) logs once and streams on.
|
||||||
|
//! Off Linux this is a no-op: macOS/Windows hosts manage their own power assertions.
|
||||||
|
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
|
/// RAII share of the host-wide inhibitor — hold one per live session/stream.
|
||||||
|
pub struct StreamHold(());
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
count: u32,
|
||||||
|
/// The logind inhibitor pipe fd — inhibition lasts exactly as long as it stays open.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fd: Option<ashpd::zbus::zvariant::OwnedFd>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state() -> &'static Mutex<State> {
|
||||||
|
static S: OnceLock<Mutex<State>> = OnceLock::new();
|
||||||
|
S.get_or_init(|| {
|
||||||
|
Mutex::new(State {
|
||||||
|
count: 0,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fd: None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take a share; the underlying inhibitor is acquired on the 0→1 edge.
|
||||||
|
pub fn hold() -> StreamHold {
|
||||||
|
let mut st = state().lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
st.count += 1;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if st.count == 1 && st.fd.is_none() {
|
||||||
|
st.fd = acquire();
|
||||||
|
}
|
||||||
|
StreamHold(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for StreamHold {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut st = state().lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
st.count = st.count.saturating_sub(1);
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if st.count == 0 && st.fd.take().is_some() {
|
||||||
|
tracing::info!("released the sleep/idle inhibitor (no live sessions)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One logind `Inhibit` call on a dedicated plain thread — zbus's blocking API must not run on
|
||||||
|
/// a tokio worker (its internal `block_on` panics there), and callers of [`hold`] may be either.
|
||||||
|
/// The join blocks the caller for the D-Bus round-trip (~ms), which every call site tolerates.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn acquire() -> Option<ashpd::zbus::zvariant::OwnedFd> {
|
||||||
|
let fd = std::thread::spawn(|| -> Option<ashpd::zbus::zvariant::OwnedFd> {
|
||||||
|
use ashpd::zbus;
|
||||||
|
// zbus's blocking API is configured out by ashpd's feature set — drive the async API on
|
||||||
|
// a private current-thread runtime instead (still on this plain thread; see fn doc).
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.ok()?;
|
||||||
|
let attempt: zbus::Result<zbus::zvariant::OwnedFd> = rt.block_on(async {
|
||||||
|
let conn = zbus::Connection::system().await?;
|
||||||
|
let reply = conn
|
||||||
|
.call_method(
|
||||||
|
Some("org.freedesktop.login1"),
|
||||||
|
"/org/freedesktop/login1",
|
||||||
|
Some("org.freedesktop.login1.Manager"),
|
||||||
|
"Inhibit",
|
||||||
|
&("sleep:idle", "Punktfunk", "a client is streaming", "block"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
reply.body().deserialize()
|
||||||
|
});
|
||||||
|
match attempt {
|
||||||
|
Ok(fd) => Some(fd),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
error = %e,
|
||||||
|
"could not take a logind sleep/idle inhibitor — the box may auto-suspend \
|
||||||
|
under a passive (video-only) viewer"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
if fd.is_some() {
|
||||||
|
tracing::info!("holding a logind sleep/idle inhibitor while clients stream");
|
||||||
|
}
|
||||||
|
fd
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user