[PR #537] fix(rest): pause the old session's pipeline on resume #509

Open
opened 2026-10-15 13:01:07 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/games-on-whales/wolf/pull/537
Author: @jdonaldson
Created: 10/10/2026
Status: 🔄 Open

Base: stable ← Head: fix/resume-pause-old-pipeline


📝 Commits (1)

  • 136f0d1 fix(rest): pause the old session's pipeline on resume

📊 Changes

1 file changed (+8 additions, -0 deletions)

View changed files

📝 src/moonlight-server/rest/endpoints.hpp (+8 -0)

📄 Description

Follow-up to #501 / #504 / #510, on the re-entry path.

What resume() does today (rest/endpoints.hpp): it creates a new session, moves the old session's wayland_display and input devices onto it, and replaces the old record in running_sessions. It never fires an event for the old session, so the old session's video and audio pipelines are left to whatever the control channel did.

Where pipelines actually get paused: only control/control.cpp, on ENET_EVENT_TYPE_DISCONNECT or on a TERMINATION control packet. Both depend on the client getting a message out before it goes away. When that doesn't happen (link drop, client crash, a phone going to sleep), the old pipeline is still running on the display the new session is about to take over, and since its session id has just left running_sessions, nothing can stop it by id afterwards. That is the state in which the second pipeline's CUDA context creation collides with the first one's (the GsCUDABuf / apply_video_info panic in #501). #505 guards the cached-context side of that race; this closes the other side.

Change: fire PauseStreamEvent{old_session->session_id} before creating the new session. It's the same event the control channel fires on disconnect, so the behaviour on resume is now the same whether or not the TERMINATION packet made it.

Why this is safe

  • PauseStreamEvent only sends EOS to the pipeline (streaming.cpp, pause_handler); the handler's own comment states that killing the pipeline and re-creating it on resume is the only option. The app container and the Wayland display are untouched, which is what resume relies on.
  • The EOS quits run_pipeline's main loop and the handler array is destroyed with it, so if the control channel already paused this session the second event finds no handler and is a no-op. No double-teardown.
  • The new pipeline is only created on the later RTSP SETUP, so the old one has the HTTPS round trip to wind down first. That is the same slack a control-channel pause gets today.

Verification

  • Compiles clean: incremental ninja wolf in the wolf-builder stage of docker/wolf.Dockerfile with this header swapped in (base tree stable@2273b2e + #510, since that's the builder image I had cached; the resume() body is identical on current stable).
  • Behaviour change verified by reading. I have not yet reproduced a dropped-TERMINATION resume end to end on hardware; the #504 repro (close with Moonlight's ✕, reconnect, repeat) exercises the path where the TERMINATION does arrive, so it should be unaffected.
  • No test added: as with #510, this path needs a paired client and a live session that the suite doesn't stand up.

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/games-on-whales/wolf/pull/537 **Author:** [@jdonaldson](https://github.com/jdonaldson) **Created:** 10/10/2026 **Status:** 🔄 Open **Base:** `stable` ← **Head:** `fix/resume-pause-old-pipeline` --- ### 📝 Commits (1) - [`136f0d1`](https://github.com/games-on-whales/wolf/commit/136f0d12bdacbfbbcea27ed93f744dded030e604) fix(rest): pause the old session's pipeline on resume ### 📊 Changes **1 file changed** (+8 additions, -0 deletions) <details> <summary>View changed files</summary> 📝 `src/moonlight-server/rest/endpoints.hpp` (+8 -0) </details> ### 📄 Description Follow-up to #501 / #504 / #510, on the re-entry path. **What `resume()` does today** (`rest/endpoints.hpp`): it creates a new session, moves the old session's `wayland_display` and input devices onto it, and replaces the old record in `running_sessions`. It never fires an event for the old session, so the old session's video and audio pipelines are left to whatever the control channel did. **Where pipelines actually get paused**: only `control/control.cpp`, on `ENET_EVENT_TYPE_DISCONNECT` or on a `TERMINATION` control packet. Both depend on the client getting a message out before it goes away. When that doesn't happen (link drop, client crash, a phone going to sleep), the old pipeline is still running on the display the new session is about to take over, and since its session id has just left `running_sessions`, nothing can stop it by id afterwards. That is the state in which the second pipeline's CUDA context creation collides with the first one's (the `GsCUDABuf` / `apply_video_info` panic in #501). #505 guards the cached-context side of that race; this closes the other side. **Change**: fire `PauseStreamEvent{old_session->session_id}` before creating the new session. It's the same event the control channel fires on disconnect, so the behaviour on resume is now the same whether or not the TERMINATION packet made it. **Why this is safe** - `PauseStreamEvent` only sends EOS to the pipeline (`streaming.cpp`, `pause_handler`); the handler's own comment states that killing the pipeline and re-creating it on resume is the only option. The app container and the Wayland display are untouched, which is what resume relies on. - The EOS quits `run_pipeline`'s main loop and the handler array is destroyed with it, so if the control channel already paused this session the second event finds no handler and is a no-op. No double-teardown. - The new pipeline is only created on the later RTSP `SETUP`, so the old one has the HTTPS round trip to wind down first. That is the same slack a control-channel pause gets today. **Verification** - Compiles clean: incremental `ninja wolf` in the `wolf-builder` stage of `docker/wolf.Dockerfile` with this header swapped in (base tree `stable@2273b2e` + #510, since that's the builder image I had cached; the `resume()` body is identical on current `stable`). - Behaviour change verified by reading. I have not yet reproduced a dropped-TERMINATION resume end to end on hardware; the #504 repro (close with Moonlight's ✕, reconnect, repeat) exercises the path where the TERMINATION *does* arrive, so it should be unaffected. - No test added: as with #510, this path needs a paired client and a live session that the suite doesn't stand up. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
games-on-whales/wolf#509
No description provided.