feat(host/hooks): per-app prep/undo commands (M2b — Sunshine prep-cmd parity)
apple / swift (push) Successful in 1m18s
apple / screenshots (push) Successful in 4m22s
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 1m3s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 33s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 5m3s
android / android (push) Successful in 17m6s
arch / build-publish (push) Successful in 16m51s
deb / build-publish (push) Successful in 12m16s
ci / rust (push) Successful in 25m22s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m16s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m17s
windows-host / package (push) Has been cancelled
docker / deploy-docs (push) Failing after 19s

`prep: [{"do": …, "undo": …}]` arrays on GameStream apps.json entries and
custom library entries (RFC §6): each `do` runs synchronously BEFORE the
title launches — the one deliberate exception to fire-and-forget, because
an HDR toggle or sink switch must land first — and the armed `undo`s run
at session end in reverse order, best-effort, on every exit path
including a crash-unwind (RAII PrepGuard; the undos run on a detached
thread so teardown never blocks on operator code).

- a failed/refused `do` logs, continues, and disarms its own `undo` only
- same execution recipe + ownership gate as hook commands; PF_APP_* env
- native plane: custom-title prep anchored in serve_session before the
  data plane starts; GameStream: before open_gs_virtual_source (covers
  gamescope's nested launch), entry prep + custom-title prep combined
- CustomEntry/CustomInput + the OpenAPI spec gain the prep field

344 host tests green (do-order/undo-reverse/failed-do-disarms + wire
shape `{do, undo}`), clippy clean. On-glass with a real client session
owed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-16 22:02:35 +02:00
parent 384f8e00aa
commit 63efe0ecd5
6 changed files with 276 additions and 13 deletions
@@ -21,6 +21,9 @@ pub struct AppEntry {
/// library ([`crate::library`]). When set, the launch path resolves + launches it against the
/// host's own library instead of running [`cmd`](Self::cmd). `None` for Desktop / apps.json entries.
pub library_id: Option<String>,
/// Per-app prep/undo steps (RFC §6, Sunshine `prep-cmd` parity): each `do` runs before the
/// app launches, each `undo` at stream end in reverse order (see [`crate::hooks::run_prep`]).
pub prep: Vec<crate::hooks::PrepCmd>,
}
fn config_path() -> Option<std::path::PathBuf> {
@@ -68,6 +71,12 @@ fn base_catalog() -> Vec<AppEntry> {
.and_then(parse_compositor),
cmd: it.get("cmd").and_then(|c| c.as_str()).map(String::from),
library_id: None,
// `"prep": [{"do": …, "undo": …}, …]` — optional; a malformed
// array is ignored (the entry still launches, just unprepped).
prep: it
.get("prep")
.and_then(|p| serde_json::from_value(p.clone()).ok())
.unwrap_or_default(),
})
})
.collect();
@@ -88,6 +97,7 @@ fn base_catalog() -> Vec<AppEntry> {
compositor: None,
cmd: None,
library_id: None,
prep: Vec::new(),
}];
if which("gamescope") {
if which("steam") {
@@ -97,6 +107,7 @@ fn base_catalog() -> Vec<AppEntry> {
compositor: Some(crate::vdisplay::Compositor::Gamescope),
cmd: Some("steam -gamepadui".into()),
library_id: None,
prep: Vec::new(),
});
}
if which("vkcube") {
@@ -106,6 +117,7 @@ fn base_catalog() -> Vec<AppEntry> {
compositor: Some(crate::vdisplay::Compositor::Gamescope),
cmd: Some("vkcube".into()),
library_id: None,
prep: Vec::new(),
});
}
}
@@ -139,6 +151,7 @@ fn append_library(apps: &mut Vec<AppEntry>) {
compositor: None, // auto-detect the desktop session (Windows ignores the compositor)
cmd: None,
library_id: Some(g.id),
prep: Vec::new(),
});
}
}
@@ -242,6 +255,7 @@ mod tests {
compositor: None,
cmd: None,
library_id: None,
prep: Vec::new(),
}];
append_library(&mut apps);
let ids: Vec<u32> = apps.iter().map(|a| a.id).collect();