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:
@@ -141,6 +141,25 @@ fn run(
|
|||||||
)
|
)
|
||||||
.context("capture virtual output")?;
|
.context("capture virtual output")?;
|
||||||
capturer.set_active(true);
|
capturer.set_active(true);
|
||||||
|
// Launch the app's command now that capture is live, for the backends that DON'T nest it via
|
||||||
|
// set_launch_command above: Windows (no gamescope) and Linux kwin/mutter/wlroots (which stream
|
||||||
|
// the existing desktop, so the app must be spawned into the session to land on the streamed
|
||||||
|
// output). Linux gamescope already nested it via set_launch_command, so skip it there.
|
||||||
|
#[cfg(windows)]
|
||||||
|
let launch_here = true;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let launch_here = compositor != crate::vdisplay::Compositor::Gamescope;
|
||||||
|
#[cfg(any(windows, target_os = "linux"))]
|
||||||
|
if launch_here {
|
||||||
|
if let Some(cmd) = app
|
||||||
|
.and_then(|a| a.cmd.as_deref())
|
||||||
|
.filter(|c| !c.trim().is_empty())
|
||||||
|
{
|
||||||
|
if let Err(e) = crate::library::launch_gamestream_command(cmd) {
|
||||||
|
tracing::warn!(command = %cmd, error = %e, "gamestream: could not launch app");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return stream_body(&mut *capturer, &sock, cfg, running, force_idr, rfi_range);
|
return stream_body(&mut *capturer, &sock, cfg, running, force_idr, rfi_range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1436,6 +1436,38 @@ fn steam_exe() -> Option<std::path::PathBuf> {
|
|||||||
None
|
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.
|
/// The full library: every store's titles merged + the custom entries, sorted by title.
|
||||||
pub fn all_games() -> Vec<GameEntry> {
|
pub fn all_games() -> Vec<GameEntry> {
|
||||||
let mut games = SteamProvider.list();
|
let mut games = SteamProvider.list();
|
||||||
|
|||||||
Reference in New Issue
Block a user