| author | |
| committer | |
| log | c574cadb7f1d18457fb810169f3ec9cd230b9be6 |
| tree | e6d31451fbcbeebcd0f85a9aa9c9b24ad6461883 |
| parent | d8e439ce5e7a3acbcc02653af384400b9a5052de |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
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.54 files changed, 103 insertions(+), 18 deletions(-)
crates/notebook/src/live/share.rs+22-13| ... | ... | @@ -215,8 +215,9 @@ pub struct Host { |
| 215 | 215 | reach: Option<Reach>, |
| 216 | 216 | relay: Option<String>, |
| 217 | 217 | sharing: Mutex<Sharing>, |
| 218 | room: Live, | |
| 219 | pairing: Mutex<Live>, | |
| 218 | /// The share's room and the code's, until it stops. | |
| 219 | room: Mutex<Option<Live>>, | |
| 220 | pairing: Mutex<Option<Live>>, | |
| 220 | 221 | served: Arc<Served>, |
| 221 | 222 | events: Arc<dyn Fn() + Send + Sync>, |
| 222 | 223 | } |
| ... | ... | @@ -269,20 +270,21 @@ impl Host { |
| 269 | 270 | notebook: notebook.to_owned(), |
| 270 | 271 | reach, |
| 271 | 272 | relay: relay.map(str::to_owned), |
| 272 | pairing: Mutex::new(pair(&me, &sharing, notebook, reach, relay, &events)?), | |
| 273 | pairing: Mutex::new(Some(pair(&me, &sharing, notebook, reach, relay, &events)?)), | |
| 273 | 274 | me, |
| 274 | 275 | sharing: Mutex::new(sharing), |
| 275 | room, | |
| 276 | room: Mutex::new(Some(room)), | |
| 276 | 277 | served, |
| 277 | 278 | events, |
| 278 | 279 | }; |
| 279 | 280 | Ok(host) |
| 280 | 281 | } |
| 281 | 282 | |
| 282 | /// The code guests type, once it has its number. A code with too many wrong tries is | |
| 283 | /// replaced by one with new words. | |
| 283 | /// The code guests type, once it has its number, and none once stopped. A code with too | |
| 284 | /// many wrong tries is replaced by one with new words. | |
| 284 | 285 | pub fn code(&self) -> Option<String> { |
| 285 | 286 | let mut pairing = self.pairing.lock().unwrap(); |
| 287 | let pairing = pairing.as_mut()?; | |
| 286 | 288 | if pairing.burned() { |
| 287 | 289 | let mut sharing = self.sharing.lock().unwrap(); |
| 288 | 290 | sharing.code = words().ok()?; |
| ... | ... | @@ -312,16 +314,20 @@ impl Host { |
| 312 | 314 | |
| 313 | 315 | /// How the relay last answered the code's room. |
| 314 | 316 | pub fn relayed(&self) -> Relayed { |
| 315 | self.pairing.lock().unwrap().relayed() | |
| 317 | let pairing = self.pairing.lock().unwrap(); | |
| 318 | pairing.as_ref().map_or(Relayed::Unknown, Live::relayed) | |
| 316 | 319 | } |
| 317 | 320 | |
| 318 | 321 | /// The peers in the share's room. |
| 319 | 322 | pub fn guests(&self) -> Vec<Peer> { |
| 320 | self.room.peers() | |
| 323 | let room = self.room.lock().unwrap(); | |
| 324 | room.as_ref().map(Live::peers).unwrap_or_default() | |
| 321 | 325 | } |
| 322 | 326 | |
| 323 | 327 | pub fn set_presence(&self, presence: Presence) { |
| 324 | self.room.set_presence(presence); | |
| 328 | if let Some(room) = &*self.room.lock().unwrap() { | |
| 329 | room.set_presence(presence); | |
| 330 | } | |
| 325 | 331 | } |
| 326 | 332 | |
| 327 | 333 | /// Tells every guest the files at these catalog paths changed. |
| ... | ... | @@ -329,10 +335,13 @@ impl Host { |
| 329 | 335 | self.served.tell(paths); |
| 330 | 336 | } |
| 331 | 337 | |
| 332 | /// Stops sharing: every guest hears so and is let go. | |
| 333 | pub fn stop(self) { | |
| 334 | drop(self.pairing); | |
| 335 | self.room.leave(STOPPED); | |
| 338 | /// Stops sharing: no one new is welcomed, and every guest hears so and is let go. | |
| 339 | pub fn stop(&self) { | |
| 340 | drop(self.pairing.lock().unwrap().take()); | |
| 341 | let room = self.room.lock().unwrap().take(); | |
| 342 | if let Some(room) = room { | |
| 343 | room.leave(STOPPED); | |
| 344 | } | |
| 336 | 345 | } |
| 337 | 346 | } |
| 338 | 347 |
crates/notebook/tests/live_share.rs+25| ... | ... | @@ -506,3 +506,28 @@ fn a_flooding_guest_is_hung_up_on() { |
| 506 | 506 | std::fs::read(folder.join("Garden.one")).unwrap() |
| 507 | 507 | ); |
| 508 | 508 | } |
| 509 | ||
| 510 | /// Stopping a share lets every guest go, saying so, and leaves its code and secret for no one: | |
| 511 | /// the code no longer opens anything, and sharing again makes new ones. | |
| 512 | #[test] | |
| 513 | fn stopping_lets_every_guest_go_and_retires_the_code() { | |
| 514 | let directory = tempfile::tempdir().unwrap(); | |
| 515 | let folder = notebook(directory.path()); | |
| 516 | let url = relay(Default::default()); | |
| 517 | let sharing = Sharing::new("").unwrap(); | |
| 518 | let host = host(&folder, &directory.path().join("host"), &sharing, &url); | |
| 519 | let code = code(&host); | |
| 520 | let (guest, _notebook) = guest("Grace", &code, &url, &directory.path().join("grace")); | |
| 521 | host.stop(); | |
| 522 | until("the guest never heard the host stop", || { | |
| 523 | guest.stopped() && guest.host().is_none() | |
| 524 | }); | |
| 525 | assert!(host.code().is_none() && host.guests().is_empty()); | |
| 526 | assert!(matches!( | |
| 527 | share::join(hello("Alan"), &code, "", None, Some(&url)).unwrap_err(), | |
| 528 | Refusal::NoOne | Refusal::TimedOut | |
| 529 | )); | |
| 530 | let again = Sharing::new("").unwrap(); | |
| 531 | assert!(again.secret != sharing.secret && again.share != sharing.share); | |
| 532 | assert_ne!(again.code, sharing.code); | |
| 533 | } |
crates/snowbound/src/live.rs+8-5| ... | ... | @@ -474,11 +474,14 @@ impl State { |
| 474 | 474 | background.on_touched(None); |
| 475 | 475 | } |
| 476 | 476 | if let Some(host) = self.peers.hosts.remove(location) { |
| 477 | crate::spawn(move || { | |
| 478 | if let Some(host) = Arc::into_inner(host) { | |
| 479 | host.stop(); | |
| 480 | } | |
| 481 | }); | |
| 477 | crate::spawn(move || host.stop()); | |
| 478 | } | |
| 479 | // Kept, the share would start again on the next frame, as after a relaunch. | |
| 480 | if let Some(sharing) = &mut self.peers.sharing | |
| 481 | && sharing.remove(location).is_some() | |
| 482 | && let Err(error) = keep(&kept(&self.cache, HOSTING), sharing) | |
| 483 | { | |
| 484 | eprintln!("Keeping what this computer shares: {error}"); | |
| 482 | 485 | } |
| 483 | 486 | } |
| 484 | 487 |
crates/snowbound/tests/replay.rs+48| ... | ... | @@ -33,6 +33,16 @@ impl Drop for Scratch { |
| 33 | 33 | /// returns the trees each |
| 34 | 34 | /// `accessibility` step named by the trailing word wrote. |
| 35 | 35 | fn replay(scratch: &Scratch, notebook: Option<&Path>, steps: &[&str]) -> Vec<String> { |
| 36 | replay_with(scratch, notebook, steps, &[]) | |
| 37 | } | |
| 38 | ||
| 39 | /// `replay` with these variables set for the app. | |
| 40 | fn replay_with( | |
| 41 | scratch: &Scratch, | |
| 42 | notebook: Option<&Path>, | |
| 43 | steps: &[&str], | |
| 44 | variables: &[(&str, &str)], | |
| 45 | ) -> Vec<String> { | |
| 36 | 46 | let dir = &scratch.0; |
| 37 | 47 | if let Some(source) = notebook { |
| 38 | 48 | for entry in std::fs::read_dir(source).unwrap() { |
| ... | ... | @@ -59,6 +69,7 @@ fn replay(scratch: &Scratch, notebook: Option<&Path>, steps: &[&str]) -> Vec<Str |
| 59 | 69 | command.env("SNOWBOUND_ICLOUD_FOLDER", dir.join("icloud")); |
| 60 | 70 | } |
| 61 | 71 | command |
| 72 | .envs(variables.iter().copied()) | |
| 62 | 73 | .env("HOME", dir.join("home")) |
| 63 | 74 | .env("SNOWBOUND_REPLAY", dir.join("script")) |
| 64 | 75 | .arg("--notebook") |
| ... | ... | @@ -501,3 +512,40 @@ fn a_replay_opens_only_the_notebooks_and_icloud_folder_it_is_given() { |
| 501 | 512 | listed.sort_unstable(); |
| 502 | 513 | assert_eq!(listed, ["Cloudy", "notebook"], "{tree}"); |
| 503 | 514 | } |
| 515 | ||
| 516 | /// Stop Sharing ends the share: Live Share offers Start Sharing again, and nothing is kept to | |
| 517 | /// share the notebook again on the next launch. | |
| 518 | #[cfg(feature = "live")] | |
| 519 | #[test] | |
| 520 | fn stop_sharing_ends_the_share() { | |
| 521 | let scratch = Scratch::new("stop-sharing"); | |
| 522 | let notebook = | |
| 523 | Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/cross-container/candidate"); | |
| 524 | let mut steps = vec!["modifiers command shift", "key p", "modifiers", "settle"]; | |
| 525 | steps.extend(["type Live Share", "settle", "key Enter", "settle"]); | |
| 526 | steps.extend(["key Enter", "wait 1500", "accessibility shared"]); | |
| 527 | // Copy, then Stop Sharing. | |
| 528 | steps.extend([ | |
| 529 | "key Tab", | |
| 530 | "key Tab", | |
| 531 | "key Enter", | |
| 532 | "wait 1500", | |
| 533 | "accessibility stopped", | |
| 534 | ]); | |
| 535 | // Off every network, so that the test reaches no one. | |
| 536 | let variables = [("SNOWBOUND_LIVE", "off"), ("SNOWBOUND_LIVE_RELAY", "off")]; | |
| 537 | let [shared, stopped] = replay_with(&scratch, Some(&notebook), &steps, &variables) | |
| 538 | .try_into() | |
| 539 | .unwrap(); | |
| 540 | assert!( | |
| 541 | shared.contains("Stop Sharing") && shared.contains("No one has joined yet."), | |
| 542 | "{shared}" | |
| 543 | ); | |
| 544 | assert!( | |
| 545 | stopped.contains("Start Sharing") && !stopped.contains("Stop Sharing"), | |
| 546 | "{stopped}" | |
| 547 | ); | |
| 548 | let kept = scratch.0.join("cache/live/hosting.json"); | |
| 549 | let kept = std::fs::read_to_string(kept).unwrap_or_default(); | |
| 550 | assert!(!kept.contains("code"), "still kept: {kept}"); | |
| 551 | } |