feat(gamestream): launch apps on Windows + Linux non-gamescope hosts

GameStream's apps.json `cmd` is delivered via set_launch_command, which ONLY the Linux
gamescope backend nests. On Windows (no gamescope) and Linux kwin/mutter/wlroots (which
stream the existing desktop) the command was silently dropped. Now, after capture is live,
stream.rs spawns it via library::launch_gamestream_command for those backends — Windows:
into the interactive USER session (spawn_in_active_session, since the host is SYSTEM);
Linux: a plain `sh -c` spawn into the host's own graphical session so the app lands on the
streamed (primary) output. Linux gamescope keeps nesting via set_launch_command and is
skipped here to avoid a double launch. The command is operator-typed apps.json (trusted),
never client-set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 08:12:53 +00:00
parent 5acc12d9e9
commit 7e9023faad
2 changed files with 51 additions and 0 deletions
+32
View File
@@ -1436,6 +1436,38 @@ fn steam_exe() -> Option<std::path::PathBuf> {
None
}
/// Launch a GameStream `apps.json` command (operator-typed, trusted — never client-set) into the live
/// session, AFTER capture is up. Used by the GameStream path for the backends that DON'T nest the
/// command via [`VirtualDisplay::set_launch_command`]: Windows (no gamescope) and Linux
/// kwin/mutter/wlroots (which stream the existing desktop). The caller skips this for Linux gamescope,
/// which already nested it. On Windows it runs in the interactive USER session (the host is SYSTEM);
/// on Linux the host is already inside the user's graphical session, so a plain spawn lands the app on
/// the streamed (primary) output.
#[cfg(any(windows, target_os = "linux"))]
pub fn launch_gamestream_command(cmd: &str) -> Result<()> {
let cmd = cmd.trim();
anyhow::ensure!(!cmd.is_empty(), "empty command");
#[cfg(windows)]
{
// cmd.exe /c is fine here: the value is the host operator's own apps.json command, not a
// client-influenced string (same trust as the custom-store `command` kind).
let pid = crate::interactive::spawn_in_active_session(&format!("cmd.exe /c {cmd}"), None)
.context("spawn gamestream command in the interactive session")?;
tracing::info!(command = %cmd, pid, "gamestream: launched app in the interactive session");
Ok(())
}
#[cfg(target_os = "linux")]
{
let child = std::process::Command::new("sh")
.arg("-c")
.arg(cmd)
.spawn()
.context("spawn gamestream command")?;
tracing::info!(command = %cmd, pid = child.id(), "gamestream: launched app into the session");
Ok(())
}
}
/// The full library: every store's titles merged + the custom entries, sorted by title.
pub fn all_games() -> Vec<GameEntry> {
let mut games = SteamProvider.list();