ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 1m6s
apple / swift (push) Successful in 1m24s
decky / build-publish (push) Successful in 31s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 18s
ci / bench (push) Successful in 7m2s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5m8s
deb / build-publish (push) Successful in 9m35s
arch / build-publish (push) Successful in 12m19s
deb / build-publish-host (push) Successful in 12m46s
android / android (push) Successful in 16m5s
windows-host / package (push) Successful in 16m34s
apple / screenshots (push) Successful in 6m28s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m11s
ci / rust (push) Successful in 27m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m19s
docker / deploy-docs (push) Successful in 27s
GameStream sessions outlived their client: the only complete teardowns were the explicit ones (RTSP TEARDOWN, nvhttp /cancel, mgmt DELETE /session), and the only automatic detector was a media-UDP send error — which needs an ICMP port-unreachable, so a true vanish (Wi-Fi drop, sleep, power off, crash) left video+audio encoding into the void forever, and even a plain Moonlight quit (which sends neither TEARDOWN nor /cancel) leaked the session. The stale state then cascaded: a lingering launch 503-blocked a different client under mode_conflict=reject, and streaming=true made a reconnect's PLAY take its "stream already running" branch — no new threads, old threads still aimed at the dead endpoint, the reconnect got no media. ENet already detects all of this — the control peer's reliable-ping timeout (or clean disconnect) fires Event::Disconnect within ~5-30 s — but the handler only reset input state. Wire the real teardown into it: * AppState::end_session — THE compat-plane session teardown: stops both media threads (their flags), clears launch + negotiated stream config; idempotent. /cancel and mgmt stop_session now share it. * control.rs Disconnect → end_session. Gated on the TRACKED session peer, and Connect only tracks a peer from the /launch owner's IP (the same source-IP bind the RTSP/media plane uses), so an unauthenticated LAN peer connect+disconnect can't end a live session, and a fast reconnect's stale-peer timeout can't kill its successor. * Client-unreachable UDP send errors now end the whole session via an OnSessionLost callback (built at PLAY) instead of stopping only the plane that noticed — audio no longer keeps streaming after video detects the dead client, and vice versa. Linux check/clippy/tests green (53 gamestream tests incl. the new end_session regression test). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
63 lines
2.3 KiB
Rust
63 lines
2.3 KiB
Rust
//! Session-tagged management endpoints: stop the active session, force an IDR. Split out of the
|
|
//! `mgmt` facade (plan §W5).
|
|
|
|
use super::shared::*;
|
|
use std::sync::atomic::Ordering;
|
|
|
|
/// Stop the active session
|
|
///
|
|
/// Kicks the connected client: stops the video/audio stream threads and clears the launch
|
|
/// state. Idempotent — succeeds even when nothing is streaming.
|
|
#[utoipa::path(
|
|
delete,
|
|
path = "/session",
|
|
tag = "session",
|
|
operation_id = "stopSession",
|
|
responses(
|
|
(status = NO_CONTENT, description = "Session stopped (or none was active)"),
|
|
(status = UNAUTHORIZED, description = "Missing or invalid bearer token", body = ApiError),
|
|
)
|
|
)]
|
|
pub(crate) async fn stop_session(State(st): State<Arc<MgmtState>>) -> StatusCode {
|
|
let was_streaming = st.app.end_session("management API stop");
|
|
// Native plane: the GameStream teardown above doesn't reach it (it runs its own loops off the shared
|
|
// session registry), so signal every live native session to tear down too.
|
|
let native = crate::session_status::count();
|
|
crate::session_status::stop_all();
|
|
tracing::info!(
|
|
was_streaming,
|
|
native_sessions = native,
|
|
"management API: session stopped"
|
|
);
|
|
StatusCode::NO_CONTENT
|
|
}
|
|
|
|
/// Force a keyframe
|
|
///
|
|
/// Asks the encoder for an IDR frame on the active video stream (what a client requests
|
|
/// after unrecoverable loss — exposed for debugging).
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/session/idr",
|
|
tag = "session",
|
|
operation_id = "requestIdr",
|
|
responses(
|
|
(status = ACCEPTED, description = "Keyframe requested"),
|
|
(status = UNAUTHORIZED, description = "Missing or invalid bearer token", body = ApiError),
|
|
(status = CONFLICT, description = "No active video stream", body = ApiError),
|
|
)
|
|
)]
|
|
pub(crate) async fn request_idr(State(st): State<Arc<MgmtState>>) -> Response {
|
|
let gs = st.app.streaming.load(Ordering::SeqCst);
|
|
let native = crate::session_status::count();
|
|
if !gs && native == 0 {
|
|
return api_error(StatusCode::CONFLICT, "no active video stream");
|
|
}
|
|
if gs {
|
|
st.app.force_idr.store(true, Ordering::SeqCst);
|
|
}
|
|
// Native sessions get the keyframe request through their registry flag (see `session_status`).
|
|
crate::session_status::force_idr_all();
|
|
StatusCode::ACCEPTED.into_response()
|
|
}
|