From c574cadb7f1d18457fb810169f3ec9cd230b9be6 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 2 Oct 2026 21:11:21 -0700 Subject: [PATCH] fix: Stop Sharing ends a Live Share instead of starting it again Stop let the host go but left the share among those kept for the next launch, so the next frame took it up again with the same code and secret, and its guests came straight back. Stopping now forgets it there and on disk, and Host::stop no longer needs the last handle to the host, so every guest hears the share stopped even while the dialog still shows it. Sharing again makes a new code and secret. Assisted-by: claude-opus-5.5 --- crates/notebook/src/live/share.rs | 35 +++++++++++++-------- crates/notebook/tests/live_share.rs | 25 +++++++++++++++ crates/snowbound/src/live.rs | 13 +++++--- crates/snowbound/tests/replay.rs | 48 +++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 18 deletions(-) diff --git a/crates/notebook/src/live/share.rs b/crates/notebook/src/live/share.rs index 1c0ca9025c5b18a4e40a5f7eb1b7d3463e6c00de..d9b06c802bcac13a9068ec57f761ac5d10553b30 100644 --- a/crates/notebook/src/live/share.rs +++ b/crates/notebook/src/live/share.rs @@ -215,8 +215,9 @@ pub struct Host { reach: Option, relay: Option, sharing: Mutex, - room: Live, - pairing: Mutex, + /// The share's room and the code's, until it stops. + room: Mutex>, + pairing: Mutex>, served: Arc, events: Arc, } @@ -269,20 +270,21 @@ impl Host { notebook: notebook.to_owned(), reach, relay: relay.map(str::to_owned), - pairing: Mutex::new(pair(&me, &sharing, notebook, reach, relay, &events)?), + pairing: Mutex::new(Some(pair(&me, &sharing, notebook, reach, relay, &events)?)), me, sharing: Mutex::new(sharing), - room, + room: Mutex::new(Some(room)), served, events, }; Ok(host) } - /// The code guests type, once it has its number. A code with too many wrong tries is - /// replaced by one with new words. + /// The code guests type, once it has its number, and none once stopped. A code with too + /// many wrong tries is replaced by one with new words. pub fn code(&self) -> Option { let mut pairing = self.pairing.lock().unwrap(); + let pairing = pairing.as_mut()?; if pairing.burned() { let mut sharing = self.sharing.lock().unwrap(); sharing.code = words().ok()?; @@ -312,16 +314,20 @@ impl Host { /// How the relay last answered the code's room. pub fn relayed(&self) -> Relayed { - self.pairing.lock().unwrap().relayed() + let pairing = self.pairing.lock().unwrap(); + pairing.as_ref().map_or(Relayed::Unknown, Live::relayed) } /// The peers in the share's room. pub fn guests(&self) -> Vec { - self.room.peers() + let room = self.room.lock().unwrap(); + room.as_ref().map(Live::peers).unwrap_or_default() } pub fn set_presence(&self, presence: Presence) { - self.room.set_presence(presence); + if let Some(room) = &*self.room.lock().unwrap() { + room.set_presence(presence); + } } /// Tells every guest the files at these catalog paths changed. @@ -329,10 +335,13 @@ impl Host { self.served.tell(paths); } - /// Stops sharing: every guest hears so and is let go. - pub fn stop(self) { - drop(self.pairing); - self.room.leave(STOPPED); + /// Stops sharing: no one new is welcomed, and every guest hears so and is let go. + pub fn stop(&self) { + drop(self.pairing.lock().unwrap().take()); + let room = self.room.lock().unwrap().take(); + if let Some(room) = room { + room.leave(STOPPED); + } } } diff --git a/crates/notebook/tests/live_share.rs b/crates/notebook/tests/live_share.rs index 268c76faa5fcc94d61a0d14d799182a10089aad2..f9b571d6a424c5eab94916dde3e9e7b0df90e067 100644 --- a/crates/notebook/tests/live_share.rs +++ b/crates/notebook/tests/live_share.rs @@ -506,3 +506,28 @@ fn a_flooding_guest_is_hung_up_on() { std::fs::read(folder.join("Garden.one")).unwrap() ); } + +/// Stopping a share lets every guest go, saying so, and leaves its code and secret for no one: +/// the code no longer opens anything, and sharing again makes new ones. +#[test] +fn stopping_lets_every_guest_go_and_retires_the_code() { + let directory = tempfile::tempdir().unwrap(); + let folder = notebook(directory.path()); + let url = relay(Default::default()); + let sharing = Sharing::new("").unwrap(); + let host = host(&folder, &directory.path().join("host"), &sharing, &url); + let code = code(&host); + let (guest, _notebook) = guest("Grace", &code, &url, &directory.path().join("grace")); + host.stop(); + until("the guest never heard the host stop", || { + guest.stopped() && guest.host().is_none() + }); + assert!(host.code().is_none() && host.guests().is_empty()); + assert!(matches!( + share::join(hello("Alan"), &code, "", None, Some(&url)).unwrap_err(), + Refusal::NoOne | Refusal::TimedOut + )); + let again = Sharing::new("").unwrap(); + assert!(again.secret != sharing.secret && again.share != sharing.share); + assert_ne!(again.code, sharing.code); +} diff --git a/crates/snowbound/src/live.rs b/crates/snowbound/src/live.rs index 1d573b7e7a071e0cb4a3c905293e506ffbea17a0..c34418286839f8b82cb4156d84c2a6cd4946574d 100644 --- a/crates/snowbound/src/live.rs +++ b/crates/snowbound/src/live.rs @@ -474,11 +474,14 @@ impl State { background.on_touched(None); } if let Some(host) = self.peers.hosts.remove(location) { - crate::spawn(move || { - if let Some(host) = Arc::into_inner(host) { - host.stop(); - } - }); + crate::spawn(move || host.stop()); + } + // Kept, the share would start again on the next frame, as after a relaunch. + if let Some(sharing) = &mut self.peers.sharing + && sharing.remove(location).is_some() + && let Err(error) = keep(&kept(&self.cache, HOSTING), sharing) + { + eprintln!("Keeping what this computer shares: {error}"); } } diff --git a/crates/snowbound/tests/replay.rs b/crates/snowbound/tests/replay.rs index 79ca0e308730a3001109642b6767aa49fa738d2e..151f02b0b4cde9eabedc913015e9515fdab01722 100644 --- a/crates/snowbound/tests/replay.rs +++ b/crates/snowbound/tests/replay.rs @@ -33,6 +33,16 @@ impl Drop for Scratch { /// returns the trees each /// `accessibility` step named by the trailing word wrote. fn replay(scratch: &Scratch, notebook: Option<&Path>, steps: &[&str]) -> Vec { + replay_with(scratch, notebook, steps, &[]) +} + +/// `replay` with these variables set for the app. +fn replay_with( + scratch: &Scratch, + notebook: Option<&Path>, + steps: &[&str], + variables: &[(&str, &str)], +) -> Vec { let dir = &scratch.0; if let Some(source) = notebook { for entry in std::fs::read_dir(source).unwrap() { @@ -59,6 +69,7 @@ fn replay(scratch: &Scratch, notebook: Option<&Path>, steps: &[&str]) -> Vec