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.
This commit is contained in:
@@ -640,20 +640,31 @@ final class SessionModel: ObservableObject {
|
||||
guard let conn = connection else { return }
|
||||
let name = activeHost?.displayName ?? "host"
|
||||
// WHY it ended, asked while the connection is still up — `disconnect` tears it down.
|
||||
// The host closes with APP_EXITED when the game it launched for this session quit, which is
|
||||
// a normal finish the player just performed, not a failure to report.
|
||||
let gameExited = conn.endedBecauseGameExited
|
||||
let reason = conn.sessionEndReason
|
||||
// Where a game exit sends us: back into the library this title was launched from, so the
|
||||
// next one is a tap away. Only for a launch that CAME from the library — a game exiting in
|
||||
// a plain desktop session has no library to return to.
|
||||
let host = activeHost
|
||||
let cameFromLibrary = launchedTitleID != nil
|
||||
disconnect(deliberate: false) // host/network ended it — keep the linger for a reconnect
|
||||
if gameExited {
|
||||
switch reason {
|
||||
case .gameExited:
|
||||
// The player quit their own game. Not a failure, and they are probably after the next
|
||||
// title — so no banner, and back to the library it came from.
|
||||
if cameFromLibrary, let host {
|
||||
returnToLibrary = host
|
||||
}
|
||||
} else {
|
||||
case .hostEnded, .local:
|
||||
// Someone asked for this: an operator "End" on the host, or our own close racing in.
|
||||
// Say it plainly, without the error framing.
|
||||
errorMessage = "\(name) ended the session."
|
||||
case .hostError:
|
||||
errorMessage = "\(name) ended the session with an error."
|
||||
case .lost:
|
||||
errorMessage = "Lost the connection to \(name)."
|
||||
case .none:
|
||||
// No verdict (an older core, or the close raced the read): keep the wording this path
|
||||
// has always used rather than inventing one.
|
||||
errorMessage = "Session ended by \(name)."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1430,24 +1430,49 @@ public final class PunktfunkConnection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Did this session end because **the game the host launched for it exited**, rather than the
|
||||
/// stream dropping out?
|
||||
/// Why a stream session ended — the Swift mirror of `PunktfunkEndReason` (ABI v17).
|
||||
///
|
||||
/// Only meaningful once the session HAS ended (a plane threw `.closed`, or `onSessionEnd`
|
||||
/// fired) — before that it is simply false. False also covers every other ending: a user stop,
|
||||
/// the host going away, network loss, an idle timeout. Read it before tearing the connection
|
||||
/// down; once `close()` has been requested this reports false like any other ending, which is
|
||||
/// the safe direction (the caller falls back to its normal end-of-session handling).
|
||||
///
|
||||
/// A game ending is a normal finish, not a failure — that is the whole point of asking. See
|
||||
/// `punktfunk_connection_game_exited` (ABI v17).
|
||||
public var endedBecauseGameExited: Bool {
|
||||
guard let h = liveHandle() else { return false }
|
||||
var out: UInt8 = 0
|
||||
guard punktfunk_connection_game_exited(h, &out) == statusOK else { return false }
|
||||
return out != 0
|
||||
/// The distinction that matters to a UI is normal vs alarming, and it is not a spectrum: a
|
||||
/// player quitting their game and a host falling off the network both arrive as "the session
|
||||
/// ended". Without this every client wrote one message for all of them, and every client chose
|
||||
/// an error.
|
||||
public enum SessionEndReason: UInt8, Sendable {
|
||||
/// Not ended, or ended before a reason could be observed. Also the fallback for an
|
||||
/// unrecognized value — the core may be newer than this code.
|
||||
case none = 0
|
||||
/// This client closed the session. Nothing to report: the UI initiated it.
|
||||
case local = 1
|
||||
/// The host's launched game exited. A normal finish, and the one reason worth acting on:
|
||||
/// go back to the library the title was launched from.
|
||||
case gameExited = 2
|
||||
/// The host ended the session deliberately (an operator "End", or it simply finished).
|
||||
case hostEnded = 3
|
||||
/// The host closed reporting a failure of its own.
|
||||
case hostError = 4
|
||||
/// The connection died rather than being closed: idle timeout, reset, network gone. This —
|
||||
/// and only this — is the "the host may be asleep" case.
|
||||
case lost = 5
|
||||
|
||||
/// Is this an ordinary outcome rather than something to alarm the user about? `.none`
|
||||
/// counts as normal: no evidence of trouble is not evidence of it.
|
||||
public var isNormal: Bool { self != .hostError && self != .lost }
|
||||
}
|
||||
|
||||
/// Why this session ended. Only meaningful once it HAS ended (a plane threw `.closed`, or
|
||||
/// `onSessionEnd` fired) — before that it is `.none`.
|
||||
///
|
||||
/// Read it before tearing the connection down: once `close()` has been requested this reports
|
||||
/// `.none`, which is the safe direction (the caller falls back to its normal handling).
|
||||
public var sessionEndReason: SessionEndReason {
|
||||
guard let h = liveHandle() else { return .none }
|
||||
var out: UInt8 = 0
|
||||
guard punktfunk_connection_end_reason(h, &out) == statusOK else { return .none }
|
||||
return SessionEndReason(rawValue: out) ?? .none
|
||||
}
|
||||
|
||||
/// Shorthand for the single most actionable reason: the host's launched game exited.
|
||||
public var endedBecauseGameExited: Bool { sessionEndReason == .gameExited }
|
||||
|
||||
deinit { close() }
|
||||
|
||||
/// Snapshot the handle unless close is pending (callers hold their plane lock).
|
||||
|
||||
@@ -908,7 +908,27 @@ fn pump(
|
||||
}
|
||||
}
|
||||
Err(PunktfunkError::NoFrame) => {}
|
||||
Err(PunktfunkError::Closed) => break Some("Host ended the session".to_string()),
|
||||
// The session ended. `None` here means "normal finish" to every embedder — the browse
|
||||
// console returns to the library with no status strip, the one-shot binary exits 0
|
||||
// quietly — so only an ending that actually went wrong should carry a message.
|
||||
// Previously EVERY close reported "Host ended the session", which put an error-shaped
|
||||
// line in front of the player for quitting their own game.
|
||||
Err(PunktfunkError::Closed) => {
|
||||
use punktfunk_core::client::PunktfunkEndReason as End;
|
||||
break match connector.end_reason() {
|
||||
// The player quit the game the host launched. Nothing to report; a launcher
|
||||
// embedder returns to its library, which is where they were headed anyway.
|
||||
End::GameExited => None,
|
||||
// We closed it, or the host closed cleanly (an operator "End", or the session
|
||||
// simply finishing). Both were asked for.
|
||||
End::Local | End::HostEnded => None,
|
||||
End::HostError => Some("The host ended the session with an error".to_string()),
|
||||
End::Lost => Some("Connection lost".to_string()),
|
||||
// No verdict (an older core, or the close raced the read): keep the wording
|
||||
// this arm has always used rather than inventing a new one.
|
||||
End::None => Some("Host ended the session".to_string()),
|
||||
};
|
||||
}
|
||||
Err(e) => break Some(format!("session: {e:?}")),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user