Worktree field kleisty triage #69
Merged
enricobuehler
merged 6 commits from 2026-08-06 12:41:30 +00:00
worktree-field-kleisty-triage into main
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
72777119fd |
fix(client/android): stop reporting every disconnect as a lost connection
ci / web (pull_request) Successful in 1m6s
apple / swift (pull_request) Successful in 1m30s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m50s
android / android (pull_request) Successful in 3m43s
ci / rust-arm64 (pull_request) Successful in 4m26s
ci / rust (pull_request) Successful in 7m12s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 12m38s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 8m23s
The stream watchdog polled a bare "has the session ended" boolean, so it had exactly one thing it could say and said it every time: "Connection lost — the host may be asleep. Wake it to reconnect." That ran when the player quit their game, when an operator ended the session from the console, and when they pressed Back themselves — telling them to go wake a host that was never asleep. It now reads the end reason. Only a connection that actually died gets that line, a host-side failure gets its own, and the three deliberate endings say nothing at all: leaving the stream is already the feedback, and a toast on top of it is just noise. A game launched from a library also returns to that library instead of host selection, which needs the intent hoisted out of the console shell: the stream replaces that shell in the composition, discarding the `remember`s holding its screen and host, so by the time the session ends there is nothing left to navigate back with. The parent holds it across the gap and the shell consumes it on the way in. The touch UI has no library — only the console shell does — so there it is the toast fix alone. |
||
|
|
81b4f76c4d |
fix(client): a session ending on purpose stops reading as a failure
The desktop clients turned every host-side close into "Host ended the session", and a reason string means "abnormal" to everything downstream: the GTK and Windows shells raised a banner, the console overlay drew a status strip. Quitting a game you launched yourself produced all of that. Now only a host error or a lost connection carries a message; the deliberate endings return the silence those shells already give a clean exit, which is also what puts the console back in its library with nothing in the way. The Apple client gains the same distinction. It had one line for every ending — "Session ended by <host>." — which is fine for an operator stopping the session and wrong for a link that died, so each now says what happened. A game exiting stays silent and returns to the library it was launched from. Both read the reason while the connection is still up, because tearing it down is what makes it unreadable, and both fall back to their previous wording when there is no verdict — an older core, or a close that raced the read — rather than inventing a new one for a case they cannot see. |
||
|
|
ec44496285 |
feat(client): tell clients WHY a session ended, not just that it did
A session ending was a single bit. A player quitting their game, an operator ending the session from the console, a stop the client itself asked for, a host crashing and a Wi-Fi drop all arrived as the same "closed" — so every client had to write one message covering all of them, and every client picked an error. That is how quitting your own game came to be reported as trouble on all three. The information was already there and thrown away: the host closes with APP_EXITED when a launched game exits, with 0 when it ends the session cleanly and 1 when it fails, and a link that simply dies never closes at all. The connection watcher now classifies that into a PunktfunkEndReason — local, game exited, host ended, host error, lost — and latches it before the shutdown flag, since the two are read by different threads and the reason must never arrive second. Exposed as punktfunk_connection_end_reason. This replaces the game-exited flag added a moment ago rather than joining it: that question is one row of this table, and it was never released. Still additive to any embedder that ignores it, and the host sends the same bytes either way, so the wire is untouched. `is_normal()` is the question nearly every caller actually has, so both the Rust and C surfaces answer it directly rather than making each client re-derive which of five values are worth alarming a user about. |
||
|
|
d4dd5f7a3d |
feat(client): a game exiting takes you back to its library
Quit a game you launched from a host's library and the stream ended with "Session ended by <host>." on the host-selection screen — an error report for something you had just done on purpose, and several taps away from starting the next title. The host has always said what happened: it closes the connection with APP_EXITED when the game it launched for a session exits, and that code's own documentation describes this feature. Nothing ever read it — a search across every client found zero consumers. (It also could not reach anyone until the previous commit, since the close only happens once the lease declares the game gone.) The core now records the reason as it observes the close, latched before the shutdown flag because different threads watch the two, and exposes it as punktfunk_connection_game_exited. Purely additive: a client that never asks behaves exactly as before, the host sends identical bytes, and the wire version is untouched — ABI 17. The Apple client asks while the connection is still up, then treats a game exit as the normal finish it is: no error banner, and if the session began as a library launch it reopens that library so the next title is one tap away. Any other ending — a stop, the host going away, network loss — is unchanged. The other clients keep their existing end-of-session behaviour; the call is there when they want it. |
||
|
|
ea762b849d |
fix(client/ios): Escape stays in the game instead of freeing the pointer
Pressing Escape mid-stream on an iPad handed the mouse back to iPadOS: the captured cursor was swapped for the system one and the game stopped receiving relative motion, so aiming died until you clicked back in. Two previous attempts treated that release as unavoidable and built recovery around it — a re-lock burst, then a click that re-asks. Both came back from the field unchanged, because both fought the release after it had already happened, inside the cooldown the platform applies straight after its own "let me out" gesture. The release was never unavoidable. This app had no UIKit key handling at all: every key arrives on the GameController path, which is a parallel HID feed that does not consume the UIKit event, and the only thing that ever became first responder was the video view, and only to summon the soft keyboard. So every hardware Escape reached UIKit unclaimed — and an unclaimed key press is precisely what lets the system apply its own default for that key. Apps that read a hardware keyboard the ordinary way consume the event as a side effect and never see this. So claim it. The stream controller becomes first responder while capture is engaged and takes Escape in pressesBegan/pressesEnded, passing every other press to super untouched. Escape still reaches the host on the GameController path, so in-game menus open exactly as before; only the system's own interpretation is suppressed. Scoped to captured input, so Escape keeps dismissing sheets and leaving full screen whenever the stream doesn't own the keyboard, and the deliberate ways out are untouched — Cmd-Escape and Ctrl-Opt-Shift-Q are read off the same GameController path and clear capture themselves. The recovery path stays as a backstop and is retimed to match what was measured: the old burst spent its entire budget within ~0.6 s of the drop, i.e. wholly inside the cooldown, where the answer can only be no. Retries now continue at 1.2 s and 2.4 s, and quietly — they don't hide the cursor or mute pointer motion the way the burst does, so a longer recovery costs nothing when it fails. |
||
|
|
76e8bd1b98 |
fix(host/gamelease): a game that exited stops counting as running
When a launched game's processes are all gone, the watcher asks one last out-of-band question before ending the session: does the launcher still think the game is up? On Windows that reads Steam's per-app `Running` registry flag. It was only ever meant to be a tie-breaker for a scan that momentarily can't see the game — a launcher re-execing, an engine relaunching itself into a new pid. It had no bound. Honouring the flag reset the confirm window every pass, so a flag Steam left set — it does that whenever it doesn't cleanly observe the exit: it crashed, it was closed first, the game re-parented — pinned the lease in `running` for the life of the host. The console kept showing the game, `session_on_game_exit` never fired, and the only way to get the stream back was a manual "End". Reported from the field on Windows 0.24.0. `steam_running_hint` also believes the FIRST hive that says so, so a stale flag in any loaded profile was enough. The absence timer now keeps running instead of being reset, and that is what bounds it: past `VETO_LIMIT` (30 s) with nothing of the game on the box, the launcher's opinion is stale rather than early and the session ends anyway, logged at WARN so it is visible. Ending a moment early is the cheaper failure — the stream drops while the game lives, the user reconnects, and nothing is ever killed. Ending never was the bug. The rule is now a pure `exit_confirmed(gone_for, hint_running)` with a test. The watch loop polls a live process table and can't be unit-tested, which is exactly how an unbounded veto shipped unnoticed. |