feat(session/gamestream): the compat plane learns a game's lifetime too
Moonlight sessions had neither direction of the session⇄game binding: a game exiting left the player looking at a desktop, and a session ending never touched the game. The machinery for both landed with the native plane; this wires it up. The compat plane had no way to say "this is over" — RTSP carries no close code — so `/cancel`, the management stop and a game exiting all looked exactly like a client vanishing. They now set a session quit flag, which is what lets the virtual display skip its keep-alive linger for a real stop and what the end-game-on-session-end policy reads to tell a decision from a network blip. A drop still lingers, and still gets its reconnect window. Moonlight can't resume a session, so a client coming back for a game it left behind does it by launching the title again — that reclaims the waiting game before anything starts, rather than letting the old window close on the copy the player is now holding. Both planes now resolve a launch through one lookup (`resolve_launch`), so a GameStream title arrives with the same identity and the same detect signals a native one does; `resolve_session_launch`/`launch_command` were the older, identity-less half of that and are gone. A Moonlight game also has no live-session entry to hang off, so the status surface gains a slot for it — without it the Dashboard would show a stream with no game while one was running. Gates on .21: check + clippy --all-targets clean, 296 tests (293 + 3), fmt CI-parity.
This commit is contained in:
@@ -879,30 +879,6 @@ fn session_watcher_loop(tx: std::sync::mpsc::Sender<SessionSwitch>, stop: Arc<At
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the end-game-on-session-end policy when the session's video loop exits, whichever way it
|
||||
/// exits (`return`, `?`, a `break` out of the loop, panic-unwind) — the same RAII shape the per-title
|
||||
/// prep/undo guard uses, for the same reason: teardown paths are many and easy to miss one of.
|
||||
///
|
||||
/// Reading `quit` at **drop** time is the point: it is what distinguishes a client that deliberately
|
||||
/// stopped (or an operator who did) from one that merely vanished, and that distinction is the whole
|
||||
/// safety story for `Always` — a vanish gets a reconnect window, a deliberate stop does not.
|
||||
struct GameLifetime {
|
||||
lease: crate::gamelease::GameLease,
|
||||
quit: Arc<AtomicBool>,
|
||||
/// Hex client fingerprint, so a reconnecting client can reclaim its own game and nothing else.
|
||||
fingerprint: Option<String>,
|
||||
}
|
||||
|
||||
impl Drop for GameLifetime {
|
||||
fn drop(&mut self) {
|
||||
crate::gamelease::on_session_end(
|
||||
&self.lease,
|
||||
self.quit.load(Ordering::SeqCst),
|
||||
self.fingerprint.as_deref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// All per-session inputs for [`virtual_stream`], bundled so the session entry
|
||||
/// is one moved value instead of a 13-positional-argument `#[allow(too_many_arguments)]` signature
|
||||
/// (Goal-1 stage 4, plan §2.4). Everything is **owned** — the receivers move in (`virtual_stream` is their
|
||||
@@ -1328,10 +1304,12 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
||||
// order): `session.ended` fires first, then the game policy runs — the order an operator reading
|
||||
// the log expects. The fingerprint is what lets a reconnecting client reclaim its own game and
|
||||
// nothing else.
|
||||
let _game_life = game_lease.map(|lease| GameLifetime {
|
||||
lease,
|
||||
quit: quit.clone(),
|
||||
fingerprint: endpoint::peer_fingerprint(&conn).map(hex::encode),
|
||||
let _game_life = game_lease.map(|lease| {
|
||||
crate::gamelease::SessionGuard::new(
|
||||
lease,
|
||||
quit.clone(),
|
||||
endpoint::peer_fingerprint(&conn).map(hex::encode),
|
||||
)
|
||||
});
|
||||
|
||||
let perf = pf_host_config::config().perf;
|
||||
|
||||
Reference in New Issue
Block a user