core: pull the ticket off presence before the reap wait on host fault
Gemini's merge-round review (P2, CERTAIN): the fault handler ran `stop_host().await` first, and a host that merely closed stdout but lives on — trapped SIGINT, wedged — makes that call burn the full 2 s stop grace before the SIGKILL fallback. For that whole window the dead share stayed advertised: peers could still click Watch on it, and the sharer's own UI kept saying "sharing". The handler now retires the share where the fault is decided, not where the corpse is confirmed: presence ticket removal and ScreenShareStopped are emitted before the reap wait, and only the explanatory error (which carries the unconfirmed-reap caveat) waits for `stop_host`. The Stopped-before-Error contract is unchanged and still gated. StopScreenShare's identical reap-then-presence ordering predates S2 and is deliberately left alone (user-initiated stop, lower stakes); recorded as a follow-up note instead of churning reviewed main-line code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+20
-12
@@ -3587,6 +3587,26 @@ async fn run_core_loop(
|
||||
"Screen share host died (stdout EOF with the share still advertised)",
|
||||
);
|
||||
current_sharing = None;
|
||||
// Pull the ticket off presence FIRST, before the reap: if the
|
||||
// child only closed stdout and lives on, `stop_host` burns the
|
||||
// full stop grace before the SIGKILL fallback, and for that
|
||||
// whole window peers would still see (and click Watch on) a
|
||||
// share whose host is already gone (Gemini S2-merge review,
|
||||
// P2-1).
|
||||
if let Some(session) = &mut active_session {
|
||||
let self_state = presence.to_state(
|
||||
is_muted.load(Ordering::Relaxed),
|
||||
net.endpoint.addr(),
|
||||
None,
|
||||
);
|
||||
let _ = session.room_state.update_self_state(self_state).await;
|
||||
}
|
||||
// Stopped next — it clears the UI's sharing state — so the
|
||||
// local UI also stops saying "sharing" before the reap wait,
|
||||
// and the error explaining why comes only after, so the user
|
||||
// is never left looking at a "sharing" UI with an error
|
||||
// beside it.
|
||||
let _ = ui_tx.send(UiEvent::ScreenShareStopped).await;
|
||||
let mut unconfirmed = false;
|
||||
if let Some(session) = &mut active_session {
|
||||
// The child is usually already dead, so this confirms the
|
||||
@@ -3598,19 +3618,7 @@ async fn run_core_loop(
|
||||
session.teardown.stop_host().await,
|
||||
Some(teardown::StopOutcome::Unconfirmed)
|
||||
);
|
||||
// Pull the ticket off presence so nobody clicks Watch on a
|
||||
// dead share.
|
||||
let self_state = presence.to_state(
|
||||
is_muted.load(Ordering::Relaxed),
|
||||
net.endpoint.addr(),
|
||||
None,
|
||||
);
|
||||
let _ = session.room_state.update_self_state(self_state).await;
|
||||
}
|
||||
// Stopped first — it clears the UI's sharing state — and only
|
||||
// then the error explaining why, so the user is never left
|
||||
// looking at a "sharing" UI with an error beside it.
|
||||
let _ = ui_tx.send(UiEvent::ScreenShareStopped).await;
|
||||
let detail = if unconfirmed {
|
||||
" Its process also couldn't be confirmed dead — check for a stray pixelpass."
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user