authorclover caruso <git@paperclover.net> 2026-10-12 22:37:02-07:00
committerclover caruso <git@paperclover.net> 2026-10-12 22:38:18-07:00
loga46bcc611e2e0f0799e9271027594e97e0df9e92
treec1e40cc1f35c7ab2f1bafc2f818d77df93114d79
parent6badb301e0d6afedf55c92da8041ef24f0a84caa
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

fix(live): fence handover control frames to current streams

Route credential rotation through authenticated access streams and serialize native welcome snapshots with their enqueue. Release browser membership locks before fallible sends and stop a superseded rotation batch. Preserve stream incarnation checks and terminal retirement. Fixes #141 Assisted-by: gpt-6.1-sol

8 files changed, 1440 insertions(+), 242 deletions(-)

crates/notebook/src/live.rs+55-24
...@@ -61,7 +61,8 @@ struct Shared {...@@ -61,7 +61,8 @@ struct Shared {
61 secret: Vec<u8>,61 secret: Vec<u8>,
62 reach: Option<Reach>,62 reach: Option<Reach>,
63 state: Mutex<State>,63 state: Mutex<State>,
64 events: Box<dyn Fn(Event) + Send + Sync>,64 #[allow(clippy::type_complexity)]
65 events: Box<dyn Fn(&Arc<Shared>, Event) + Send + Sync>,
65 stopped: AtomicBool,66 stopped: AtomicBool,
66 connections: AtomicU64,67 connections: AtomicU64,
67}68}
...@@ -231,6 +232,16 @@ impl Live {...@@ -231,6 +232,16 @@ impl Live {
231 reach: Option<Reach>,232 reach: Option<Reach>,
232 relay: Option<&str>,233 relay: Option<&str>,
233 events: impl Fn(Event) + Send + Sync + 'static,234 events: impl Fn(Event) + Send + Sync + 'static,
235 ) -> io::Result<Live> {
236 Self::start_with(me, room, reach, relay, move |_, event| events(event))
237 }
238
239 fn start_with(
240 me: Hello,
241 room: &Room,
242 reach: Option<Reach>,
243 relay: Option<&str>,
244 events: impl Fn(&Arc<Shared>, Event) + Send + Sync + 'static,
234 ) -> io::Result<Live> {245 ) -> io::Result<Live> {
235 let host = match reach {246 let host = match reach {
236 Some(Reach::Network) => IpAddr::V4(Ipv4Addr::UNSPECIFIED),247 Some(Reach::Network) => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
...@@ -440,6 +451,21 @@ impl Drop for Live {...@@ -440,6 +451,21 @@ impl Drop for Live {
440}451}
441452
442impl Shared {453impl Shared {
454 fn gone(&self, hello: &Arc<Hello>) -> bool {
455 !self.state.lock().unwrap().peers.contains_key(&hello.peer)
456 }
457
458 fn connected(&self, hello: &Arc<Hello>) -> bool {
459 !self.stopped.load(Ordering::Acquire)
460 && self
461 .state
462 .lock()
463 .unwrap()
464 .peers
465 .get(&hello.peer)
466 .is_some_and(|link| Arc::ptr_eq(&link.peer.hello, hello))
467 }
468
443 /// The peers in the room: those met directly, and the rest as the relay's group or a469 /// The peers in the room: those met directly, and the rest as the relay's group or a
444 /// stream through the relay last heard of them.470 /// stream through the relay last heard of them.
445 fn peers(&self) -> Vec<Peer> {471 fn peers(&self) -> Vec<Peer> {
...@@ -547,7 +573,7 @@ impl Shared {...@@ -547,7 +573,7 @@ impl Shared {
547573
548 /// Counts a meeting that failed on the secret: the end sharing a code burns it after too574 /// Counts a meeting that failed on the secret: the end sharing a code burns it after too
549 /// many, and an end that typed one gives up at once.575 /// many, and an end that typed one gives up at once.
550 fn failed(&self) {576 fn failed(self: &Arc<Self>) {
551 let mut state = self.state.lock().unwrap();577 let mut state = self.state.lock().unwrap();
552 state.failed += 1;578 state.failed += 1;
553 match &self.room {579 match &self.room {
...@@ -556,7 +582,7 @@ impl Shared {...@@ -556,7 +582,7 @@ impl Shared {
556 _ => {}582 _ => {}
557 }583 }
558 drop(state);584 drop(state);
559 (self.events)(Event::Changed);585 (self.events)(self, Event::Changed);
560 }586 }
561587
562 /// Meets the peer at the other end of `pipe` in room `tag`, then reads from it until it588 /// Meets the peer at the other end of `pipe` in room `tag`, then reads from it until it
...@@ -597,7 +623,7 @@ impl Shared {...@@ -597,7 +623,7 @@ impl Shared {
597 if matches!(self.room, Room::Code { owner: false, .. }) {623 if matches!(self.room, Room::Code { owner: false, .. }) {
598 self.stopped.store(true, Ordering::Release);624 self.stopped.store(true, Ordering::Release);
599 }625 }
600 (self.events)(Event::Changed);626 (self.events)(&self, Event::Changed);
601 } else if error.kind() == io::ErrorKind::InvalidData {627 } else if error.kind() == io::ErrorKind::InvalidData {
602 pipe.met(false);628 pipe.met(false);
603 self.failed();629 self.failed();
...@@ -614,17 +640,14 @@ impl Shared {...@@ -614,17 +640,14 @@ impl Shared {
614 let connection = self.connections.fetch_add(1, Ordering::Relaxed);640 let connection = self.connections.fetch_add(1, Ordering::Relaxed);
615 let (out, outgoing) = mpsc::channel();641 let (out, outgoing) = mpsc::channel();
616 let line = Line(out);642 let line = Line(out);
617 {643 let retired = {
618 let mut state = self.state.lock().unwrap();644 let mut state = self.state.lock().unwrap();
619 // A peer met both directly and through a relay keeps the direct connection, as645 // A peer met both directly and through a relay keeps the direct connection, as
620 // both ends then agree.646 // both ends then agree.
621 let kept = match state.peers.get(&peer) {647 let kept = match state.peers.get(&peer) {
622 _ if peer == self.me.peer => false,648 _ if peer == self.me.peer => false,
623 Some(link) if link.pipe.direct() || !pipe.direct() => false,649 Some(link) if link.pipe.direct() || !pipe.direct() => false,
624 Some(link) => {650 Some(_) => true,
625 link.pipe.shutdown();
626 true
627 }
628 None => true,651 None => true,
629 };652 };
630 if !kept {653 if !kept {
...@@ -632,9 +655,6 @@ impl Shared {...@@ -632,9 +655,6 @@ impl Shared {
632 pipe.shutdown();655 pipe.shutdown();
633 return false;656 return false;
634 }657 }
635 if self.carries_presence(&*pipe) {
636 let _ = line.0.send(Out::Presence);
637 }
638 state.peers.insert(658 state.peers.insert(
639 peer,659 peer,
640 Link {660 Link {
...@@ -646,13 +666,19 @@ impl Shared {...@@ -646,13 +666,19 @@ impl Shared {
646 line: line.clone(),666 line: line.clone(),
647 pipe: Arc::clone(&pipe),667 pipe: Arc::clone(&pipe),
648 },668 },
649 );669 )
670 };
671 if let Some(retired) = retired {
672 retired.pipe.shutdown();
673 }
674 if self.carries_presence(&*pipe) {
675 let _ = line.0.send(Out::Presence);
650 }676 }
651 let writing = Arc::clone(&pipe);677 let writing = Arc::clone(&pipe);
652 let shared = Arc::clone(&self);678 let shared = Arc::clone(&self);
653 thread::spawn(move || shared.write(&*writing, send, outgoing));679 thread::spawn(move || shared.write(&*writing, send, outgoing));
654 (self.events)(Event::Met(&hello, &line));680 (self.events)(&self, Event::Met(&hello, &line));
655 (self.events)(Event::Changed);681 (self.events)(&self, Event::Changed);
656 let ended = loop {682 let ended = loop {
657 let (message, body) = match receive.receive(&mut stream) {683 let (message, body) = match receive.receive(&mut stream) {
658 Ok(frame) => frame,684 Ok(frame) => frame,
...@@ -664,17 +690,22 @@ impl Shared {...@@ -664,17 +690,22 @@ impl Shared {
664 let Ok(presence) = minicbor::decode::<Presence>(&body) else {690 let Ok(presence) = minicbor::decode::<Presence>(&body) else {
665 break None;691 break None;
666 };692 };
667 if let Some(link) = self.state.lock().unwrap().peers.get_mut(&peer) {693 if let Some(link) = self.state.lock().unwrap().peers.get_mut(&peer)
694 && link.connection == connection
695 {
668 link.peer.presence = Some(presence);696 link.peer.presence = Some(presence);
669 }697 }
670 (self.events)(Event::Changed);698 (self.events)(&self, Event::Changed);
671 }699 }
672 kind => {700 kind => {
673 (self.events)(Event::Frame {701 (self.events)(
674 from: &hello,702 &self,
675 kind,703 Event::Frame {
676 body: &body,704 from: &hello,
677 });705 kind,
706 body: &body,
707 },
708 );
678 // The peer hangs up after its bye.709 // The peer hangs up after its bye.
679 if kind == kind::BYE {710 if kind == kind::BYE {
680 break None;711 break None;
...@@ -695,8 +726,8 @@ impl Shared {...@@ -695,8 +726,8 @@ impl Shared {
695 {726 {
696 state.peers.remove(&peer);727 state.peers.remove(&peer);
697 drop(state);728 drop(state);
698 (self.events)(Event::Left(&hello));729 (self.events)(&self, Event::Left(&hello));
699 (self.events)(Event::Changed);730 (self.events)(&self, Event::Changed);
700 }731 }
701 true732 true
702 }733 }
crates/notebook/src/live/relay.rs+16-13
...@@ -44,12 +44,12 @@ pub(super) fn join(shared: &Arc<Shared>, url: &str, port: u16) -> io::Result<()>...@@ -44,12 +44,12 @@ pub(super) fn join(shared: &Arc<Shared>, url: &str, port: u16) -> io::Result<()>
44}44}
4545
46/// Records how the relay answered, telling `shared`'s events where it changed.46/// Records how the relay answered, telling `shared`'s events where it changed.
47fn answered(shared: &Shared, answer: Answer) {47fn answered(shared: &Arc<Shared>, answer: Answer) {
48 let mut state = shared.state.lock().unwrap();48 let mut state = shared.state.lock().unwrap();
49 if state.relayed.as_ref() != Some(&answer) {49 if state.relayed.as_ref() != Some(&answer) {
50 state.relayed = Some(answer);50 state.relayed = Some(answer);
51 drop(state);51 drop(state);
52 (shared.events)(Event::Changed);52 (shared.events)(shared, Event::Changed);
53 }53 }
54}54}
5555
...@@ -245,7 +245,7 @@ fn session(...@@ -245,7 +245,7 @@ fn session(
245 state.code = Some(code);245 state.code = Some(code);
246 drop(state);246 drop(state);
247 shared.advertise(port);247 shared.advertise(port);
248 (shared.events)(Event::Changed);248 (shared.events)(shared, Event::Changed);
249 }249 }
250 }250 }
251 Ok(Notice::Welcome { you, members }) => {251 Ok(Notice::Welcome { you, members }) => {
...@@ -263,14 +263,14 @@ fn session(...@@ -263,14 +263,14 @@ fn session(
263 let left = (socket.group.as_ref())263 let left = (socket.group.as_ref())
264 .and_then(|group| group.members.lock().unwrap().remove(&slot));264 .and_then(|group| group.members.lock().unwrap().remove(&slot));
265 if left.is_some() {265 if left.is_some() {
266 (shared.events)(Event::Changed);266 (shared.events)(shared, Event::Changed);
267 }267 }
268 }268 }
269 Ok(Notice::Burned) => {269 Ok(Notice::Burned) => {
270 // Coming back, ask for another number.270 // Coming back, ask for another number.
271 *nameplate = None;271 *nameplate = None;
272 shared.state.lock().unwrap().burned = true;272 shared.state.lock().unwrap().burned = true;
273 (shared.events)(Event::Changed);273 (shared.events)(shared, Event::Changed);
274 }274 }
275 Err(()) => {}275 Err(()) => {}
276 },276 },
...@@ -296,7 +296,7 @@ fn session(...@@ -296,7 +296,7 @@ fn session(
296 if let Some(group) = &socket.group {296 if let Some(group) = &socket.group {
297 *group.out.lock().unwrap() = None;297 *group.out.lock().unwrap() = None;
298 group.members.lock().unwrap().clear();298 group.members.lock().unwrap().clear();
299 (shared.events)(Event::Changed);299 (shared.events)(shared, Event::Changed);
300 }300 }
301}301}
302302
...@@ -412,7 +412,7 @@ fn heard(shared: &Arc<Shared>, socket: &Arc<Socket>, tag: Option<&str>, slot: u3...@@ -412,7 +412,7 @@ fn heard(shared: &Arc<Shared>, socket: &Arc<Socket>, tag: Option<&str>, slot: u3
412 if serves {412 if serves {
413 meet(shared, socket, tag, slot, None);413 meet(shared, socket, tag, slot, None);
414 }414 }
415 (shared.events)(Event::Changed);415 (shared.events)(shared, Event::Changed);
416 }416 }
417 kind::PRESENCE => {417 kind::PRESENCE => {
418 let Ok(presence) = minicbor::decode::<Presence>(&body) else {418 let Ok(presence) = minicbor::decode::<Presence>(&body) else {
...@@ -422,17 +422,20 @@ fn heard(shared: &Arc<Shared>, socket: &Arc<Socket>, tag: Option<&str>, slot: u3...@@ -422,17 +422,20 @@ fn heard(shared: &Arc<Shared>, socket: &Arc<Socket>, tag: Option<&str>, slot: u3
422 if let Some(peer) = members.get_mut(&slot).and_then(|m| m.peer.as_mut()) {422 if let Some(peer) = members.get_mut(&slot).and_then(|m| m.peer.as_mut()) {
423 peer.presence = Some(presence);423 peer.presence = Some(presence);
424 drop(members);424 drop(members);
425 (shared.events)(Event::Changed);425 (shared.events)(shared, Event::Changed);
426 }426 }
427 }427 }
428 // A peer met directly says the rest there.428 // A peer met directly says the rest there.
429 kind if kind < 256 => {429 kind if kind < 256 => {
430 if let Some(hello) = hello.filter(|hello| !shared.direct(&hello.peer)) {430 if let Some(hello) = hello.filter(|hello| !shared.direct(&hello.peer)) {
431 (shared.events)(Event::Frame {431 (shared.events)(
432 from: &hello,432 shared,
433 kind,433 Event::Frame {
434 body: &body,434 from: &hello,
435 });435 kind,
436 body: &body,
437 },
438 );
436 }439 }
437 }440 }
438 _ => {}441 _ => {}
crates/notebook/src/live/share.rs+294-132
...@@ -18,7 +18,7 @@ use std::{...@@ -18,7 +18,7 @@ use std::{
18 io,18 io,
19 path::PathBuf,19 path::PathBuf,
20 sync::{20 sync::{
21 Arc, Condvar, Mutex,21 Arc, Condvar, Mutex, Weak,
22 atomic::{AtomicBool, AtomicU64, Ordering},22 atomic::{AtomicBool, AtomicU64, Ordering},
23 mpsc,23 mpsc,
24 },24 },
...@@ -29,6 +29,9 @@ use web_time::Instant;...@@ -29,6 +29,9 @@ use web_time::Instant;
2929
30#[path = "share/batch.rs"]30#[path = "share/batch.rs"]
31mod batch;31mod batch;
32#[cfg(all(test, not(target_arch = "wasm32")))]
33#[path = "share/handover_tests.rs"]
34mod handover_tests;
32#[path = "share/membership.rs"]35#[path = "share/membership.rs"]
33mod membership;36mod membership;
34#[cfg(target_arch = "wasm32")]37#[cfg(target_arch = "wasm32")]
...@@ -304,6 +307,7 @@ struct Served {...@@ -304,6 +307,7 @@ struct Served {
304/// many more it may start now, and the sections it read lately, newest first, as it keeps307/// many more it may start now, and the sections it read lately, newest first, as it keeps
305/// their images.308/// their images.
306struct Admitted {309struct Admitted {
310 hello: Arc<Hello>,
307 line: Line,311 line: Line,
308 queue: mpsc::SyncSender<(u16, Vec<u8>)>,312 queue: mpsc::SyncSender<(u16, Vec<u8>)>,
309 starts: f64,313 starts: f64,
...@@ -336,9 +340,32 @@ fn refused(kind: io::ErrorKind, message: &str) -> Error {...@@ -336,9 +340,32 @@ fn refused(kind: io::ErrorKind, message: &str) -> Error {
336340
337impl Served {341impl Served {
338 /// Serves `peer` on `line`, through workers of its own that end as it leaves.342 /// Serves `peer` on `line`, through workers of its own that end as it leaves.
339 fn admit(self: &Arc<Self>, peer: [u8; 16], line: &Line) {343 fn admit(
344 self: &Arc<Self>,
345 source: &Arc<super::Shared>,
346 hello: &Arc<Hello>,
347 line: &Line,
348 ) -> bool {
349 let peer = hello.peer;
340 let (queue, waiting) = mpsc::sync_channel::<(u16, Vec<u8>)>(QUEUED - WORKERS);350 let (queue, waiting) = mpsc::sync_channel::<(u16, Vec<u8>)>(QUEUED - WORKERS);
341 let waiting = Arc::new(Mutex::new(waiting));351 let waiting = Arc::new(Mutex::new(waiting));
352 let guest = Admitted {
353 hello: Arc::clone(hello),
354 line: line.clone(),
355 queue,
356 starts: BURST,
357 counted: Instant::now(),
358 held: Vec::new(),
359 };
360 let mut guests = self.guests.lock().unwrap();
361 if !source.connected(hello) {
362 drop(guests);
363 drop(guest);
364 return false;
365 }
366 let retired = guests.insert(peer, guest);
367 drop(guests);
368 drop(retired);
342 for _ in 0..WORKERS {369 for _ in 0..WORKERS {
343 let (served, waiting, line) =370 let (served, waiting, line) =
344 (Arc::downgrade(self), Arc::clone(&waiting), line.clone());371 (Arc::downgrade(self), Arc::clone(&waiting), line.clone());
...@@ -352,23 +379,20 @@ impl Served {...@@ -352,23 +379,20 @@ impl Served {
352 }379 }
353 });380 });
354 }381 }
355 let guest = Admitted {382 true
356 line: line.clone(),
357 queue,
358 starts: BURST,
359 counted: Instant::now(),
360 held: Vec::new(),
361 };
362 self.guests.lock().unwrap().insert(peer, guest);
363 }383 }
364384
365 /// Hands a request from `peer` to its workers, or hangs up on a guest that has too many385 /// Hands a request from `peer` to its workers, or hangs up on a guest that has too many
366 /// waiting or starts them too fast.386 /// waiting or starts them too fast.
367 fn queue(&self, peer: &[u8; 16], kind: u16, body: &[u8]) {387 fn queue(&self, source: &Arc<super::Shared>, hello: &Arc<Hello>, kind: u16, body: &[u8]) {
388 let peer = &hello.peer;
368 let mut guests = self.guests.lock().unwrap();389 let mut guests = self.guests.lock().unwrap();
369 let Some(guest) = guests.get_mut(peer) else {390 let Some(guest) = guests.get_mut(peer) else {
370 return;391 return;
371 };392 };
393 if !Arc::ptr_eq(&guest.hello, hello) || !source.connected(hello) {
394 return;
395 }
372 let continued = kind == kind::PUT396 let continued = kind == kind::PUT
373 || matches!(kind, kind::READ | kind::READ_FILE)397 || matches!(kind, kind::READ | kind::READ_FILE)
374 && minicbor::decode::<Request>(body).is_ok_and(|request| request.handle.is_some());398 && minicbor::decode::<Request>(body).is_ok_and(|request| request.handle.is_some());
...@@ -380,21 +404,33 @@ impl Served {...@@ -380,21 +404,33 @@ impl Served {
380 guest.starts -= 1.0;404 guest.starts -= 1.0;
381 }405 }
382 if guest.starts < 0.0 || guest.queue.try_send((kind, body.to_vec())).is_err() {406 if guest.starts < 0.0 || guest.queue.try_send((kind, body.to_vec())).is_err() {
383 guest.line.hang_up(FLOODED);407 let guest = guests.remove(peer);
384 guests.remove(peer);408 drop(guests);
409 if let Some(guest) = guest {
410 guest.line.hang_up(FLOODED);
411 }
385 }412 }
386 }413 }
387414
388 fn forget(&self, peer: &[u8; 16]) {415 fn forget(&self, peer: &[u8; 16], expected: Option<(&Arc<super::Shared>, &Arc<Hello>)>) {
389 self.guests.lock().unwrap().remove(peer);416 let retired = {
390 self.snapshots417 let mut snapshots = self.snapshots.lock().unwrap();
391 .lock()418 let mut puts = self.puts.lock().unwrap();
392 .unwrap()419 let mut guests = self.guests.lock().unwrap();
393 .retain(|(guest, _), _| guest != peer);420 if let Some((source, hello)) = expected
394 self.puts421 && (!source.gone(hello)
395 .lock()422 || !guests
396 .unwrap()423 .get(peer)
397 .retain(|(guest, _), _| guest != peer);424 .is_some_and(|guest| Arc::ptr_eq(&guest.hello, hello)))
425 {
426 return;
427 }
428 let retired = guests.remove(peer);
429 snapshots.retain(|(guest, _), _| guest != peer);
430 puts.retain(|(guest, _), _| guest != peer);
431 retired
432 };
433 drop(retired);
398 }434 }
399435
400 /// Tells every guest, and the host, that a guest changed the files at `paths`.436 /// Tells every guest, and the host, that a guest changed the files at `paths`.
...@@ -1086,10 +1122,16 @@ impl Guest {...@@ -1086,10 +1122,16 @@ impl Guest {
1086 catalog: Mutex::default(),1122 catalog: Mutex::default(),
1087 });1123 });
1088 let heard = Arc::clone(&inner);1124 let heard = Arc::clone(&inner);
1089 let live = Live::start(me, &Room::Notebook(secret), reach, relay, move |event| {1125 let live = Live::start_with(
1090 heard.heard(event);1126 me,
1091 (heard.events)();1127 &Room::Notebook(secret),
1092 })?;1128 reach,
1129 relay,
1130 move |source, event| {
1131 heard.heard(source, event);
1132 (heard.events)();
1133 },
1134 )?;
1093 Ok(Arc::new(Self { live, inner }))1135 Ok(Arc::new(Self { live, inner }))
1094 }1136 }
10951137
...@@ -1565,146 +1607,266 @@ impl Inner {...@@ -1565,146 +1607,266 @@ impl Inner {
1565 }1607 }
1566 }1608 }
15671609
1568 fn heard(self: &Arc<Self>, event: Event) {1610 fn heard(self: &Arc<Self>, source: &Arc<super::Shared>, event: Event) {
1569 let serves = |hello: &Hello| {
1570 self.host
1571 .lock()
1572 .unwrap()
1573 .as_ref()
1574 .is_some_and(|(host, _)| host.peer == hello.peer)
1575 };
1576 match event {1611 match event {
1577 Event::Met(hello, line) if hello.serves == Some(self.share) => {1612 Event::Met(hello, line) if hello.serves == Some(self.share) => {
1578 *self.host.lock().unwrap() = Some((Arc::clone(hello), line.clone()));1613 let mut host = self.host.lock().unwrap();
1614 let retired = if source.connected(hello) {
1615 host.replace((Arc::clone(hello), line.clone()))
1616 } else {
1617 None
1618 };
1619 drop(host);
1620 drop(retired);
1579 }1621 }
1580 Event::Left(hello) if serves(hello) => {1622 Event::Left(hello) => {
1581 *self.host.lock().unwrap() = None;1623 let (retired, presence, pending, reports) = {
1582 drop(self.presence.lock().unwrap().take());1624 let mut host = self.host.lock().unwrap();
1583 self.current.lock().unwrap().clear();1625 if !host
1584 // Each request waiting hears its answer was lost.1626 .as_ref()
1585 self.pending.lock().unwrap().clear();1627 .is_some_and(|(admitted, _)| Arc::ptr_eq(admitted, hello))
1586 if let Some(reports) = self.watch.lock().unwrap().take() {1628 || !source.gone(hello)
1629 {
1630 return;
1631 }
1632 let retired = host.take();
1633 let presence = self.presence.lock().unwrap().take();
1634 self.current.lock().unwrap().clear();
1635 let pending = std::mem::take(&mut *self.pending.lock().unwrap());
1636 let reports = self.watch.lock().unwrap().take();
1637 (retired, presence, pending, reports)
1638 };
1639 drop(retired);
1640 drop(presence);
1641 drop(pending);
1642 if let Some(reports) = reports {
1587 reports.lost();1643 reports.lost();
1588 }1644 }
1589 }1645 }
1590 Event::Frame {1646 Event::Frame { from, kind, body } => {
1591 from, kind, body, ..1647 let host = self.host.lock().unwrap();
1592 } => match kind {1648 if !host
1593 kind::WELCOME if serves(from) => {1649 .as_ref()
1594 if let Ok(welcome) = minicbor::decode::<Welcome>(body) {1650 .is_some_and(|(admitted, _)| Arc::ptr_eq(admitted, from))
1595 self.join_presence(welcome.room);1651 || !source.connected(from)
1596 }1652 {
1653 return;
1597 }1654 }
1598 kind::REPLY if serves(from) => {1655 match kind {
1599 if let Ok(reply) = minicbor::decode::<Reply>(body)1656 kind::WELCOME => {
1600 && let Some(waiting) = self.pending.lock().unwrap().remove(&reply.id)1657 if let Ok(welcome) = minicbor::decode::<Welcome>(body) {
1601 {1658 drop(host);
1602 let _ = waiting.send(reply);1659 self.join_presence(source, from, welcome.room);
1660 }
1603 }1661 }
1604 }1662 kind::REPLY => {
1605 kind::DELTA if serves(from) => {1663 if let Ok(reply) = minicbor::decode::<Reply>(body) {
1606 if let Ok(delta) = minicbor::decode::<Delta>(body) {1664 let waiting = self.pending.lock().unwrap().remove(&reply.id);
1607 self.apply(&delta);1665 drop(host);
1666 if let Some(waiting) = waiting {
1667 let _ = waiting.send(reply);
1668 }
1669 }
1608 }1670 }
1609 }1671 kind::DELTA => {
1610 kind::TOUCHED if serves(from) => {1672 if let Ok(delta) = minicbor::decode::<Delta>(body) {
1611 if let Ok(touched) = minicbor::decode::<Touched>(body) {1673 self.apply(&delta);
1612 self.touched(&touched);
1613 let reports = self.watch.lock().unwrap().clone();
1614 if let Some(reports) = reports {
1615 reports.touched(&touched.paths);
1616 }1674 }
1617 }1675 }
1618 }1676 kind::TOUCHED => {
1619 kind::BYE if serves(from) => {1677 if let Ok(touched) = minicbor::decode::<Touched>(body) {
1620 if let Ok(bye) = minicbor::decode::<wire::Bye>(body) {1678 self.touched(&touched);
1621 let ended = match bye.reason.as_str() {1679 let reports = self.watch.lock().unwrap().clone();
1622 STOPPED => Some(Ended::Stopped),1680 drop(host);
1623 "removed" => Some(Ended::Removed),1681 if let Some(reports) = reports {
1624 _ => None,1682 reports.touched(&touched.paths);
1625 };1683 }
1626 if ended.is_some() {1684 }
1627 *self.ended.lock().unwrap() = ended;1685 }
1686 kind::BYE => {
1687 if let Ok(bye) = minicbor::decode::<wire::Bye>(body) {
1688 let ended = match bye.reason.as_str() {
1689 STOPPED => Some(Ended::Stopped),
1690 "removed" => Some(Ended::Removed),
1691 _ => None,
1692 };
1693 if ended.is_some() {
1694 *self.ended.lock().unwrap() = ended;
1695 }
1628 }1696 }
1629 }1697 }
1698 _ => {}
1630 }1699 }
1631 _ => {}1700 }
1632 },
1633 _ => {}1701 _ => {}
1634 }1702 }
1635 }1703 }
16361704
1637 fn join_presence(self: &Arc<Self>, secret: [u8; 16]) {1705 fn join_presence(
1638 let mut presence = self.presence.lock().unwrap();1706 self: &Arc<Self>,
1639 if presence.as_ref().is_some_and(|(held, _)| *held == secret) {1707 source: &Arc<super::Shared>,
1640 return;1708 from: &Arc<Hello>,
1709 secret: [u8; 16],
1710 ) {
1711 {
1712 let host = self.host.lock().unwrap();
1713 if !host
1714 .as_ref()
1715 .is_some_and(|(admitted, _)| Arc::ptr_eq(admitted, from))
1716 || !source.connected(from)
1717 || self
1718 .presence
1719 .lock()
1720 .unwrap()
1721 .as_ref()
1722 .is_some_and(|(held, _)| *held == secret)
1723 {
1724 return;
1725 }
1641 }1726 }
1642 let inner = Arc::downgrade(self);1727 let inner = Arc::downgrade(self);
1643 let connected = Mutex::new(None);1728 let connected = Mutex::new(None);
1644 let live = Live::start(1729 let live = Live::start_with(
1645 self.me.clone(),1730 self.me.clone(),
1646 &Room::Notebook(secret),1731 &Room::Notebook(secret),
1647 self.reach,1732 self.reach,
1648 self.relay.as_deref(),1733 self.relay.as_deref(),
1649 move |event| {1734 move |source, event| {
1650 if let Some(inner) = inner.upgrade() {1735 let Some(inner) = inner.upgrade() else {
1651 if matches!(event, Event::Changed) {1736 return;
1652 let mut connected = connected.lock().unwrap();1737 };
1653 let host = inner1738 let reports = {
1654 .host1739 let host = inner.host.lock().unwrap();
1655 .lock()1740 let presence = inner.presence.lock().unwrap();
1656 .unwrap()1741 let Some((_, live)) = presence.as_ref() else {
1657 .as_ref()1742 return;
1658 .map(|(host, _)| host.peer);1743 };
1659 let peers = inner1744 #[cfg(not(target_arch = "wasm32"))]
1660 .presence1745 let selected = Arc::ptr_eq(&live.shared, source);
1661 .lock()1746 #[cfg(target_arch = "wasm32")]
1662 .unwrap()1747 let selected = Arc::ptr_eq(&live.0, source);
1663 .as_ref()1748 if !selected {
1664 .map(|(_, live)| live.peers())1749 return;
1665 .unwrap_or_default();1750 }
1666 let present =1751 match event {
1667 host.filter(|host| peers.iter().any(|peer| peer.hello.peer == *host));1752 Event::Changed => {
1668 if *connected != present {1753 let mut connected = connected.lock().unwrap();
1669 *connected = present;1754 let host = host.as_ref().map(|(host, _)| host.peer);
1670 inner.current.lock().unwrap().clear();1755 let peers = source.peers();
1671 drop(connected);1756 let present = host
1672 let reports = inner.watch.lock().unwrap().clone();1757 .filter(|host| peers.iter().any(|peer| peer.hello.peer == *host));
1673 if present.is_some()1758 if *connected == present {
1674 && let Some(reports) = reports1759 None
1675 {1760 } else {
1676 reports.touched(&[String::new()]);1761 *connected = present;
1762 inner.current.lock().unwrap().clear();
1763 if present.is_some() {
1764 inner
1765 .watch
1766 .lock()
1767 .unwrap()
1768 .clone()
1769 .map(|reports| (reports, vec![String::new()]))
1770 } else {
1771 None
1772 }
1677 }1773 }
1678 }1774 }
1679 }1775 Event::Frame { from, kind, body }
1680 if matches!(1776 if host
1681 event,1777 .as_ref()
1682 Event::Frame {1778 .is_some_and(|(host, _)| host.peer == from.peer)
1683 kind: kind::DELTA | kind::TOUCHED,1779 && source
1684 ..1780 .peers()
1781 .iter()
1782 .any(|peer| Arc::ptr_eq(&peer.hello, from)) =>
1783 {
1784 match kind {
1785 kind::DELTA => {
1786 if let Ok(delta) = minicbor::decode::<Delta>(body) {
1787 inner.apply(&delta);
1788 }
1789 None
1790 }
1791 kind::TOUCHED => {
1792 if let Ok(touched) = minicbor::decode::<Touched>(body) {
1793 inner.touched(&touched);
1794 inner
1795 .watch
1796 .lock()
1797 .unwrap()
1798 .clone()
1799 .map(|reports| (reports, touched.paths))
1800 } else {
1801 None
1802 }
1803 }
1804 _ => None,
1805 }
1685 }1806 }
1686 ) {1807 _ => None,
1687 inner.heard(event);
1688 }1808 }
1689 (inner.events)();1809 };
1810 if let Some((reports, paths)) = reports {
1811 reports.touched(&paths);
1690 }1812 }
1813 (inner.events)();
1691 },1814 },
1692 );1815 );
1693 match live {1816 match live {
1694 Ok(live) => {1817 Ok(live) => self.install_presence(source, from, secret, live),
1695 live.set_presence(self.here.lock().unwrap().clone());1818 Err(error) => eprintln!("Live Share: could not join presence: {error}"),
1696 *presence = Some((secret, live));1819 }
1697 drop(presence);1820 }
1698 let mut current = self.current.lock().unwrap();1821
1699 let paths: Vec<String> = current.keys().cloned().collect();1822 fn install_presence(
1700 current.clear();1823 self: &Arc<Self>,
1701 drop(current);1824 source: &Arc<super::Shared>,
1702 let reports = self.watch.lock().unwrap().clone();1825 from: &Arc<Hello>,
1703 if let Some(reports) = reports {1826 secret: [u8; 16],
1704 reports.touched(&paths);1827 live: Live,
1828 ) {
1829 let here = self.here.lock().unwrap();
1830 live.set_presence(here.clone());
1831 #[cfg(not(target_arch = "wasm32"))]
1832 let refresh = Arc::clone(&live.shared);
1833 #[cfg(target_arch = "wasm32")]
1834 let refresh = Arc::clone(&live.0);
1835 let mut candidate = Some(live);
1836 let (retired, paths, reports) = {
1837 let host = self.host.lock().unwrap();
1838 if host
1839 .as_ref()
1840 .is_some_and(|(admitted, _)| Arc::ptr_eq(admitted, from))
1841 && source.connected(from)
1842 {
1843 let mut presence = self.presence.lock().unwrap();
1844 if presence.as_ref().is_some_and(|(held, _)| *held == secret) {
1845 (None, Vec::new(), None)
1846 } else {
1847 let retired = presence.replace((secret, candidate.take().unwrap()));
1848 let paths = self
1849 .current
1850 .lock()
1851 .unwrap()
1852 .drain()
1853 .map(|(path, _)| path)
1854 .collect::<Vec<_>>();
1855 let reports = self.watch.lock().unwrap().clone();
1856 (retired, paths, reports)
1705 }1857 }
1858 } else {
1859 (None, Vec::new(), None)
1706 }1860 }
1707 Err(error) => eprintln!("Live Share: could not join presence: {error}"),1861 };
1862 drop(here);
1863 drop(retired);
1864 if candidate.is_none() {
1865 (refresh.events)(&refresh, Event::Changed);
1866 }
1867 drop(candidate);
1868 if let Some(reports) = reports {
1869 reports.touched(&paths);
1708 }1870 }
1709 }1871 }
1710}1872}
crates/notebook/src/live/share/batch.rs+4
...@@ -342,6 +342,10 @@ mod tests {...@@ -342,6 +342,10 @@ mod tests {
342 served.guests.lock().unwrap().insert(342 served.guests.lock().unwrap().insert(
343 peer,343 peer,
344 Admitted {344 Admitted {
345 hello: Arc::new(Hello {
346 peer,
347 ..Hello::new("Guest".into(), None).unwrap()
348 }),
345 line: crate::live::Line(line),349 line: crate::live::Line(line),
346 queue,350 queue,
347 starts: BURST,351 starts: BURST,
crates/notebook/src/live/share/handover_tests.rs created+721
...@@ -0,0 +1,721 @@
1use super::super::{GONE, OPENING, Pipe, Shared, State};
2use super::*;
3use std::{
4 io::{Read, Write},
5 net::{Shutdown, TcpListener, TcpStream},
6};
7
8struct Barrier {
9 reached: mpsc::Sender<()>,
10 resume: mpsc::Receiver<()>,
11}
12
13impl Barrier {
14 fn wait(self) -> io::Result<()> {
15 self.reached.send(()).map_err(io::Error::other)?;
16 self.resume.recv_timeout(GONE).map_err(io::Error::other)
17 }
18}
19
20fn barrier() -> (Barrier, mpsc::Receiver<()>, mpsc::Sender<()>) {
21 let (reached, waiting) = mpsc::channel();
22 let (resume, continued) = mpsc::channel();
23 (
24 Barrier {
25 reached,
26 resume: continued,
27 },
28 waiting,
29 resume,
30 )
31}
32
33struct HandoverPipe {
34 stream: TcpStream,
35 direct: bool,
36 body: Mutex<Option<Barrier>>,
37 retired: mpsc::Sender<()>,
38}
39
40impl Pipe for HandoverPipe {
41 fn read(&self, buffer: &mut [u8]) -> io::Result<usize> {
42 let gate = if buffer.len() > 4 {
43 self.body.lock().unwrap().take()
44 } else {
45 None
46 };
47 if let Some(gate) = gate {
48 // Preserve a complete copied ciphertext block across the old pipe's shutdown.
49 Read::read_exact(&mut &self.stream, buffer)?;
50 gate.wait()?;
51 Ok(buffer.len())
52 } else {
53 Read::read(&mut &self.stream, buffer)
54 }
55 }
56
57 fn write(&self, bytes: &[u8]) -> io::Result<usize> {
58 Write::write(&mut &self.stream, bytes)
59 }
60
61 fn set_read_timeout(&self, timeout: Duration) -> io::Result<()> {
62 self.stream.set_read_timeout(Some(timeout))
63 }
64
65 fn shutdown(&self) {
66 let _ = self.stream.shutdown(Shutdown::Both);
67 }
68
69 fn direct(&self) -> bool {
70 self.direct
71 }
72}
73
74impl Drop for HandoverPipe {
75 fn drop(&mut self) {
76 let _ = self.retired.send(());
77 }
78}
79
80struct Handover {
81 shared: Arc<Shared>,
82 guest: Arc<Inner>,
83 pipes: Vec<Arc<HandoverPipe>>,
84 readers: Vec<(mpsc::Receiver<()>, thread::JoinHandle<bool>)>,
85 retired: (mpsc::Sender<()>, mpsc::Receiver<()>),
86 observed: mpsc::Receiver<()>,
87 releases: Vec<mpsc::Sender<()>>,
88}
89
90impl Handover {
91 fn new(met: Option<Barrier>) -> Self {
92 let me = Hello::new("Guest".into(), None).unwrap();
93 let guest = Arc::new(Inner {
94 share: [41; 16],
95 me: me.clone(),
96 reach: None,
97 relay: None,
98 events: Arc::new(|| {}),
99 presence: Mutex::default(),
100 here: Mutex::default(),
101 host: Mutex::default(),
102 pending: Mutex::default(),
103 next: AtomicU64::new(1),
104 watch: Mutex::default(),
105 ended: Mutex::default(),
106 asked: Default::default(),
107 images: Mutex::default(),
108 current: Mutex::default(),
109 });
110 let heard = Arc::clone(&guest);
111 let met = Mutex::new(met);
112 let (events, observed) = mpsc::channel();
113 let room = Room::Notebook([42; 16]);
114 let shared = Arc::new(Shared {
115 me,
116 secret: room.secret(),
117 room,
118 reach: None,
119 state: Mutex::new(State::default()),
120 events: Box::new(move |source, event| {
121 let meeting = matches!(&event, Event::Met(..));
122 if meeting {
123 let gate = met.lock().unwrap().take();
124 if let Some(gate) = gate {
125 gate.wait().unwrap();
126 }
127 }
128 heard.heard(source, event);
129 if meeting {
130 let _ = events.send(());
131 }
132 }),
133 stopped: AtomicBool::new(false),
134 connections: AtomicU64::new(0),
135 });
136 Self {
137 shared,
138 guest,
139 pipes: Vec::new(),
140 readers: Vec::new(),
141 retired: mpsc::channel(),
142 observed,
143 releases: Vec::new(),
144 }
145 }
146
147 fn open(&mut self, host: &Hello, direct: bool) -> (TcpStream, wire::Sealer) {
148 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
149 let mut remote =
150 TcpStream::connect_timeout(&listener.local_addr().unwrap(), OPENING).unwrap();
151 remote.set_read_timeout(Some(OPENING)).unwrap();
152 remote.set_write_timeout(Some(OPENING)).unwrap();
153 let (local, _) = listener.accept().unwrap();
154 local.set_write_timeout(Some(OPENING)).unwrap();
155 let pipe = Arc::new(HandoverPipe {
156 stream: local,
157 direct,
158 body: Mutex::default(),
159 retired: self.retired.0.clone(),
160 });
161 self.pipes.push(Arc::clone(&pipe));
162 let shared = Arc::clone(&self.shared);
163 let tag = shared.room.tag().unwrap();
164 let reader_tag = tag.clone();
165 let (finished, done) = mpsc::channel();
166 let reader = thread::spawn(move || {
167 let retry = shared.run(pipe, wire::Side::Responder, &reader_tag);
168 let _ = finished.send(());
169 retry
170 });
171 self.readers.push((done, reader));
172 let (mut send, mut receive) = wire::open(
173 &mut remote,
174 wire::Side::Initiator,
175 &tag,
176 &self.shared.secret,
177 )
178 .unwrap();
179 send.send(&mut remote, kind::HELLO, host).unwrap();
180 let (message, body) = receive.receive(&mut remote).unwrap();
181 assert_eq!(message, kind::HELLO);
182 assert_eq!(minicbor::decode::<Hello>(&body).unwrap(), self.shared.me);
183 (remote, send)
184 }
185
186 fn heard(&self) {
187 self.observed.recv_timeout(OPENING).unwrap();
188 }
189}
190
191impl Drop for Handover {
192 fn drop(&mut self) {
193 self.shared.stopped.store(true, Ordering::Release);
194 for release in self.releases.drain(..) {
195 let _ = release.send(());
196 }
197 for pipe in &self.pipes {
198 pipe.shutdown();
199 }
200 self.shared.state.lock().unwrap().peers.clear();
201 let deadline = std::time::Instant::now() + OPENING;
202 for (_, reader) in &self.readers {
203 while !reader.is_finished() && std::time::Instant::now() < deadline {
204 thread::yield_now();
205 }
206 }
207 let readers_done = self.readers.iter().all(|(_, reader)| reader.is_finished());
208 self.guest.host.lock().unwrap().take();
209 let count = self.pipes.len();
210 self.pipes.clear();
211 let retired = (0..count).all(|_| self.retired.1.recv_timeout(OPENING).is_ok());
212 let mut joined = readers_done && retired;
213 if readers_done {
214 for (_, reader) in self.readers.drain(..) {
215 joined &= reader.join().is_ok();
216 }
217 }
218 if !thread::panicking() {
219 println!("HANDOVER_READERS_JOINED_AND_PIPE_OWNERS_RETIRED={joined};PIPES={count}");
220 assert!(
221 joined,
222 "handover reader/writer owners did not retire and join"
223 );
224 }
225 }
226}
227
228#[test]
229fn handover_stale_frame_cannot_end_promoted_guest() {
230 let mut fixture = Handover::new(None);
231 let mut host = Hello::new("Old host route".into(), None).unwrap();
232 host.serves = Some(fixture.guest.share);
233 let (mut old, mut send) = fixture.open(&host, false);
234 fixture.heard();
235 let (gate, copied, resume) = barrier();
236 fixture.releases.push(resume.clone());
237 *fixture.pipes[0].body.lock().unwrap() = Some(gate);
238 send.send(
239 &mut old,
240 kind::BYE,
241 &wire::Bye {
242 reason: STOPPED.into(),
243 },
244 )
245 .unwrap();
246 copied.recv_timeout(OPENING).unwrap();
247 host.name = "Promoted host route".into();
248 let (_new, _) = fixture.open(&host, true);
249 fixture.heard();
250 assert!(
251 fixture.shared.state.lock().unwrap().peers[&host.peer]
252 .pipe
253 .direct()
254 );
255 assert!(fixture.guest.ended.lock().unwrap().is_none());
256 resume.send(()).unwrap();
257 fixture.readers[0].0.recv_timeout(OPENING).unwrap();
258 let ended = *fixture.guest.ended.lock().unwrap();
259 drop(fixture);
260 println!("HANDOVER_STALE_FRAME_ENDED={ended:?}");
261 assert_eq!(ended, None, "stale frame ended the promoted guest");
262}
263
264#[test]
265fn handover_stale_met_cannot_replace_promoted_guest_line() {
266 let (gate, delayed, resume) = barrier();
267 let mut fixture = Handover::new(Some(gate));
268 fixture.releases.push(resume.clone());
269 let mut host = Hello::new("Old host route".into(), None).unwrap();
270 host.serves = Some(fixture.guest.share);
271 let (_old, _) = fixture.open(&host, false);
272 delayed.recv_timeout(OPENING).unwrap();
273 host.name = "Promoted host route".into();
274 let (_new, _) = fixture.open(&host, true);
275 let overlap = match fixture.observed.recv_timeout(OPENING) {
276 Ok(()) => {
277 assert_eq!(
278 fixture.guest.host.lock().unwrap().as_ref().unwrap().0.name,
279 host.name
280 );
281 resume.send(()).unwrap();
282 fixture.heard();
283 true
284 }
285 Err(mpsc::RecvTimeoutError::Timeout) => {
286 resume.send(()).unwrap();
287 fixture.heard();
288 fixture.heard();
289 false
290 }
291 Err(error) => panic!("handover event channel closed: {error}"),
292 };
293 assert_eq!(
294 fixture.shared.state.lock().unwrap().peers[&host.peer]
295 .peer
296 .hello
297 .name,
298 host.name
299 );
300 let selected = fixture
301 .guest
302 .host
303 .lock()
304 .unwrap()
305 .as_ref()
306 .unwrap()
307 .0
308 .name
309 .clone();
310 drop(fixture);
311 println!("HANDOVER_MET_PROMOTION_OVERLAP={overlap}");
312 assert_eq!(
313 selected, host.name,
314 "stale Met replaced the promoted guest Line"
315 );
316}
317
318impl Handover {
319 fn intercept(
320 &mut self,
321 before: impl Fn(&Arc<Shared>, &Event<'_>) + Send + Sync + 'static,
322 ) -> mpsc::Receiver<u16> {
323 let guest = Arc::clone(&self.guest);
324 let (observed, waiting) = mpsc::channel();
325 Arc::get_mut(&mut self.shared).unwrap().events = Box::new(move |source, event| {
326 let kind = match &event {
327 Event::Met(..) => kind::HELLO,
328 Event::Left(..) => kind::BYE,
329 Event::Frame { kind, .. } => *kind,
330 Event::Changed => 0,
331 };
332 before(source, &event);
333 guest.heard(source, event);
334 let _ = observed.send(kind);
335 });
336 waiting
337 }
338}
339
340fn observed(events: &mpsc::Receiver<u16>, wanted: u16) {
341 let end = std::time::Instant::now() + OPENING;
342 loop {
343 let kind = events
344 .recv_timeout(end.saturating_duration_since(std::time::Instant::now()))
345 .unwrap();
346 if kind == wanted {
347 return;
348 }
349 }
350}
351
352#[test]
353fn handover_old_left_preserves_promoted_host_and_pending_reply() {
354 let mut fixture = Handover::new(None);
355 let (gate, removed, resume) = barrier();
356 fixture.releases.push(resume.clone());
357 let gate = Mutex::new(Some(gate));
358 let events = fixture.intercept(move |_, event| {
359 if matches!(event, Event::Left(_))
360 && let Some(gate) = gate.lock().unwrap().take()
361 {
362 gate.wait().unwrap();
363 }
364 });
365 let mut host = Hello::new("Old host route".into(), None).unwrap();
366 host.serves = Some(fixture.guest.share);
367 let (old, _) = fixture.open(&host, false);
368 observed(&events, kind::HELLO);
369 old.shutdown(Shutdown::Both).unwrap();
370 removed.recv_timeout(OPENING).unwrap();
371 host.name = "Promoted host route".into();
372 let (_new, _) = fixture.open(&host, true);
373 observed(&events, kind::HELLO);
374 let (answer, waiting) = crate::task::response::<Reply>();
375 fixture.guest.pending.lock().unwrap().insert(73, answer);
376 resume.send(()).unwrap();
377 fixture.readers[0].0.recv_timeout(OPENING).unwrap();
378 let host_kept = fixture
379 .guest
380 .host
381 .lock()
382 .unwrap()
383 .as_ref()
384 .is_some_and(|(hello, _)| hello.name == host.name);
385 let pending_kept = fixture.guest.pending.lock().unwrap().contains_key(&73);
386 let answer_kept = matches!(waiting.try_recv(), Err(mpsc::TryRecvError::Empty));
387 drop(fixture);
388 assert!(
389 host_kept && pending_kept && answer_kept,
390 "retired Left cleared the promoted host or pending reply"
391 );
392}
393
394#[test]
395fn handover_selected_loss_clears_host_and_disconnects_pending_reply() {
396 let mut fixture = Handover::new(None);
397 let mut host = Hello::new("Selected host".into(), None).unwrap();
398 host.serves = Some(fixture.guest.share);
399 let (remote, _) = fixture.open(&host, true);
400 fixture.heard();
401 let (answer, waiting) = crate::task::response::<Reply>();
402 fixture.guest.pending.lock().unwrap().insert(74, answer);
403 remote.shutdown(Shutdown::Both).unwrap();
404 fixture.readers[0].0.recv_timeout(OPENING).unwrap();
405 let host_cleared = fixture.guest.host.lock().unwrap().is_none();
406 let pending_cleared = fixture.guest.pending.lock().unwrap().is_empty();
407 let answer_lost = matches!(waiting.try_recv(), Err(mpsc::TryRecvError::Disconnected));
408 drop(fixture);
409 assert!(
410 host_cleared && pending_cleared && answer_lost,
411 "selected route loss retained the host or pending reply"
412 );
413}
414
415fn served() -> (tempfile::TempDir, Arc<Served>) {
416 let directory = tempfile::tempdir().unwrap();
417 let folder = directory.path().join("notebook");
418 std::fs::create_dir(&folder).unwrap();
419 let served = Arc::new(Served {
420 storage: crate::session::Notebook::open(&folder, directory.path().join("cache"))
421 .unwrap()
422 .into_storage(),
423 images: Mutex::default(),
424 snapshots: Mutex::default(),
425 puts: Mutex::default(),
426 guests: Mutex::default(),
427 writers: Mutex::default(),
428 host: Mutex::default(),
429 room: Mutex::default(),
430 });
431 (directory, served)
432}
433
434#[test]
435fn handover_copied_host_request_cannot_enter_promoted_queue() {
436 let (_directory, served) = served();
437 let queues = Arc::new(Mutex::new(Vec::new()));
438 let mut fixture = Handover::new(None);
439 let serving = Arc::clone(&served);
440 let received = Arc::clone(&queues);
441 let events = fixture.intercept(move |source, event| match event {
442 Event::Met(hello, line) => {
443 let (queue, waiting) = mpsc::sync_channel(4);
444 let retired = serving.guests.lock().unwrap().insert(
445 hello.peer,
446 Admitted {
447 hello: Arc::clone(hello),
448 line: (*line).clone(),
449 queue,
450 starts: BURST,
451 counted: Instant::now(),
452 held: Vec::new(),
453 },
454 );
455 drop(retired);
456 received.lock().unwrap().push(waiting);
457 }
458 Event::Frame { from, kind, body } => serving.queue(source, from, *kind, body),
459 Event::Left(hello) => serving.forget(&hello.peer, Some((source, hello))),
460 _ => {}
461 });
462 let mut host = Hello::new("Old route".into(), None).unwrap();
463 host.serves = Some(fixture.guest.share);
464 let (mut old, mut send) = fixture.open(&host, false);
465 observed(&events, kind::HELLO);
466 let (gate, copied, resume) = barrier();
467 fixture.releases.push(resume.clone());
468 *fixture.pipes[0].body.lock().unwrap() = Some(gate);
469 send.send(
470 &mut old,
471 kind::PUT,
472 &Request {
473 id: 80,
474 bytes: Some(vec![1, 2, 3]),
475 ..Request::default()
476 },
477 )
478 .unwrap();
479 copied.recv_timeout(OPENING).unwrap();
480 host.name = "Promoted route".into();
481 let (mut new, mut send) = fixture.open(&host, true);
482 observed(&events, kind::HELLO);
483 resume.send(()).unwrap();
484 fixture.readers[0].0.recv_timeout(OPENING).unwrap();
485 let no_stale_request = matches!(
486 queues.lock().unwrap()[1].try_recv(),
487 Err(mpsc::TryRecvError::Empty)
488 );
489 send.send(
490 &mut new,
491 kind::PUT,
492 &Request {
493 id: 81,
494 bytes: Some(vec![4, 5, 6]),
495 ..Request::default()
496 },
497 )
498 .unwrap();
499 let (kind, body) = queues.lock().unwrap()[1].recv_timeout(OPENING).unwrap();
500 let accepted = kind == kind::PUT && minicbor::decode::<Request>(&body).unwrap().id == 81;
501 served.guests.lock().unwrap().clear();
502 drop(fixture);
503 assert!(
504 no_stale_request && accepted,
505 "retired request entered the promoted queue or current request was lost"
506 );
507}
508
509#[test]
510fn handover_old_left_preserves_promoted_host_uploads_and_snapshots() {
511 let (_directory, served) = served();
512 let mut fixture = Handover::new(None);
513 let (gate, removed, resume) = barrier();
514 fixture.releases.push(resume.clone());
515 let gate = Mutex::new(Some(gate));
516 let serving = Arc::clone(&served);
517 let waiting = Arc::new(Mutex::new(Vec::new()));
518 let keep_queues = Arc::clone(&waiting);
519 let events = fixture.intercept(move |source, event| match event {
520 Event::Met(hello, line) => {
521 let (queue, received) = mpsc::sync_channel(1);
522 let retired = serving.guests.lock().unwrap().insert(
523 hello.peer,
524 Admitted {
525 hello: Arc::clone(hello),
526 line: (*line).clone(),
527 queue,
528 starts: BURST,
529 counted: Instant::now(),
530 held: Vec::new(),
531 },
532 );
533 drop(retired);
534 keep_queues.lock().unwrap().push(received);
535 }
536 Event::Left(hello) => {
537 if let Some(gate) = gate.lock().unwrap().take() {
538 gate.wait().unwrap();
539 }
540 serving.forget(&hello.peer, Some((source, hello)));
541 }
542 _ => {}
543 });
544 let mut host = Hello::new("Old route".into(), None).unwrap();
545 host.serves = Some(fixture.guest.share);
546 let (old, _) = fixture.open(&host, false);
547 observed(&events, kind::HELLO);
548 old.shutdown(Shutdown::Both).unwrap();
549 removed.recv_timeout(OPENING).unwrap();
550 host.name = "Promoted route".into();
551 let (_new, _) = fixture.open(&host, true);
552 observed(&events, kind::HELLO);
553 served
554 .puts
555 .lock()
556 .unwrap()
557 .insert((host.peer, 90), vec![7, 8, 9]);
558 served
559 .snapshots
560 .lock()
561 .unwrap()
562 .insert((host.peer, 91), (Arc::new(vec![10, 11]), Instant::now()));
563 resume.send(()).unwrap();
564 fixture.readers[0].0.recv_timeout(OPENING).unwrap();
565 let guest_kept = served
566 .guests
567 .lock()
568 .unwrap()
569 .get(&host.peer)
570 .is_some_and(|guest| guest.hello.name == host.name);
571 let upload_kept = served.puts.lock().unwrap().get(&(host.peer, 90)) == Some(&vec![7, 8, 9]);
572 let snapshot_kept = served
573 .snapshots
574 .lock()
575 .unwrap()
576 .get(&(host.peer, 91))
577 .is_some_and(|(bytes, _)| **bytes == vec![10, 11]);
578 served.guests.lock().unwrap().clear();
579 drop(fixture);
580 assert!(
581 guest_kept && upload_kept && snapshot_kept,
582 "retired Left removed promoted host resources"
583 );
584}
585
586#[test]
587fn handover_deferred_presence_cannot_replace_promoted_host_room() {
588 let mut fixture = Handover::new(None);
589 let mut host = Hello::new("Old host route".into(), None).unwrap();
590 host.serves = Some(fixture.guest.share);
591 let (_old, _) = fixture.open(&host, false);
592 fixture.heard();
593 let from = Arc::clone(&fixture.guest.host.lock().unwrap().as_ref().unwrap().0);
594 let secret = [61; 16];
595 let live = Live::start(
596 fixture.guest.me.clone(),
597 &Room::Notebook(secret),
598 None,
599 None,
600 |_| {},
601 )
602 .unwrap();
603 let guest = Arc::clone(&fixture.guest);
604 let source = Arc::clone(&fixture.shared);
605 let here_owner = Arc::clone(&fixture.guest);
606 let here = here_owner.here.lock().unwrap();
607 let (entered, started) = mpsc::channel();
608 let (finished, done) = mpsc::channel();
609 let installer = thread::spawn(move || {
610 entered.send(()).unwrap();
611 guest.install_presence(&source, &from, secret, live);
612 let _ = finished.send(());
613 true
614 });
615 fixture.readers.push((done, installer));
616 started.recv_timeout(OPENING).unwrap();
617 host.name = "Promoted host route".into();
618 let (_new, _) = fixture.open(&host, true);
619 fixture.heard();
620 drop(here);
621 fixture.readers[1].0.recv_timeout(OPENING).unwrap();
622 let refused = fixture.guest.presence.lock().unwrap().is_none();
623 let promoted = fixture
624 .guest
625 .host
626 .lock()
627 .unwrap()
628 .as_ref()
629 .is_some_and(|(hello, _)| hello.name == host.name);
630 drop(fixture);
631 assert!(
632 refused && promoted,
633 "deferred old WELCOME installed its presence room"
634 );
635}
636
637#[test]
638fn handover_presence_install_seeds_latest_here() {
639 let mut fixture = Handover::new(None);
640 let mut host = Hello::new("Current host".into(), None).unwrap();
641 host.serves = Some(fixture.guest.share);
642 let (_remote, _) = fixture.open(&host, true);
643 fixture.heard();
644 let from = Arc::clone(&fixture.guest.host.lock().unwrap().as_ref().unwrap().0);
645 let secret = [62; 16];
646 let live = Live::start(
647 fixture.guest.me.clone(),
648 &Room::Notebook(secret),
649 None,
650 None,
651 |_| {},
652 )
653 .unwrap();
654 let guest = Arc::clone(&fixture.guest);
655 let source = Arc::clone(&fixture.shared);
656 let mut here = fixture.guest.here.lock().unwrap();
657 let latest = Presence {
658 section: Some([63; 16]),
659 ..Presence::default()
660 };
661 let (entered, started) = mpsc::channel();
662 let (finished, done) = mpsc::channel();
663 let installer = thread::spawn(move || {
664 entered.send(()).unwrap();
665 guest.install_presence(&source, &from, secret, live);
666 let _ = finished.send(());
667 true
668 });
669 fixture.readers.push((done, installer));
670 started.recv_timeout(OPENING).unwrap();
671 *here = latest.clone();
672 drop(here);
673 fixture.readers[1].0.recv_timeout(OPENING).unwrap();
674 let seeded = fixture
675 .guest
676 .presence
677 .lock()
678 .unwrap()
679 .as_ref()
680 .is_some_and(|(held, live)| {
681 *held == secret && live.shared.state.lock().unwrap().presence == latest
682 });
683 let installed = fixture.guest.presence.lock().unwrap().take();
684 drop(installed);
685 drop(fixture);
686 assert!(
687 seeded,
688 "presence installation overwrote current here with a stale seed"
689 );
690}
691
692pub(super) fn promoted_routes<T>(
693 room: Room,
694 before: impl FnOnce(&Arc<Shared>, &Arc<Hello>, &Line),
695 check: impl FnOnce(&Arc<Shared>, &Arc<Hello>, &Line, &Arc<Hello>, &Line) -> T,
696) -> T {
697 let mut fixture = Handover::new(None);
698 let shared = Arc::get_mut(&mut fixture.shared).unwrap();
699 shared.secret = room.secret();
700 shared.room = room;
701 let host = Hello::new("Pairing device".into(), None).unwrap();
702 let (_old, _) = fixture.open(&host, false);
703 fixture.heard();
704 let (old, old_line) = {
705 let state = fixture.shared.state.lock().unwrap();
706 let link = &state.peers[&host.peer];
707 (Arc::clone(&link.peer.hello), link.line.clone())
708 };
709 before(&fixture.shared, &old, &old_line);
710 let (_new, _) = fixture.open(&host, true);
711 fixture.heard();
712 let (new, new_line) = {
713 let state = fixture.shared.state.lock().unwrap();
714 let link = &state.peers[&host.peer];
715 (Arc::clone(&link.peer.hello), link.line.clone())
716 };
717 let result = check(&fixture.shared, &old, &old_line, &new, &new_line);
718 drop((old_line, new_line));
719 drop(fixture);
720 result
721}
crates/notebook/src/live/share/membership.rs+265-37
...@@ -13,7 +13,7 @@ struct Members {...@@ -13,7 +13,7 @@ struct Members {
13 relay: Option<String>,13 relay: Option<String>,
14 sharing: Mutex<Sharing>,14 sharing: Mutex<Sharing>,
15 changes: Mutex<()>,15 changes: Mutex<()>,
16 pending: Mutex<BTreeMap<[u8; 16], (Arc<Hello>, Line)>>,16 pending: Mutex<BTreeMap<[u8; 16], (Arc<Hello>, Line, Weak<super::super::Shared>)>>,
17 access: Mutex<HashMap<[u8; 16], Live>>,17 access: Mutex<HashMap<[u8; 16], Live>>,
18 room: Mutex<Option<Live>>,18 room: Mutex<Option<Live>>,
19 served: Arc<Served>,19 served: Arc<Served>,
...@@ -134,7 +134,7 @@ impl Host {...@@ -134,7 +134,7 @@ impl Host {
134 .lock()134 .lock()
135 .unwrap()135 .unwrap()
136 .values()136 .values()
137 .map(|(hello, _)| Arc::clone(hello))137 .map(|(hello, _, _)| Arc::clone(hello))
138 .collect()138 .collect()
139 }139 }
140140
...@@ -155,14 +155,27 @@ impl Host {...@@ -155,14 +155,27 @@ impl Host {
155 }155 }
156156
157 pub fn allow(&self, peer: &[u8; 16]) -> io::Result<()> {157 pub fn allow(&self, peer: &[u8; 16]) -> io::Result<()> {
158 let pending = self.members.pending.lock().unwrap().remove(peer);158 let request = {
159 if let Some((hello, line)) = pending {159 let mut pending = self.members.pending.lock().unwrap();
160 if !pending.get(peer).is_some_and(|(hello, _, source)| {
161 source
162 .upgrade()
163 .is_some_and(|source| source.connected(hello))
164 }) {
165 return Ok(());
166 }
167 pending.remove(peer)
168 };
169 if let Some((hello, line, source)) = request {
160 if let Err(error) = self.members.grant(&hello, &line) {170 if let Err(error) = self.members.grant(&hello, &line) {
161 self.members171 let mut pending = self.members.pending.lock().unwrap();
162 .pending172 if source
163 .lock()173 .upgrade()
164 .unwrap()174 .is_some_and(|source| source.connected(&hello))
165 .insert(*peer, (hello, line));175 && !pending.contains_key(peer)
176 {
177 pending.insert(*peer, (hello, line, source));
178 }
166 return Err(error);179 return Err(error);
167 }180 }
168 (self.members.events)();181 (self.members.events)();
...@@ -171,7 +184,8 @@ impl Host {...@@ -171,7 +184,8 @@ impl Host {
171 }184 }
172185
173 pub fn decline(&self, peer: &[u8; 16]) {186 pub fn decline(&self, peer: &[u8; 16]) {
174 if let Some((_, line)) = self.members.pending.lock().unwrap().remove(peer) {187 let request = self.members.pending.lock().unwrap().remove(peer);
188 if let Some((_, line, _)) = request {
175 let _ = line.send(kind::APPROVAL, &wire::Approval::Declined);189 let _ = line.send(kind::APPROVAL, &wire::Approval::Declined);
176 (self.members.events)();190 (self.members.events)();
177 }191 }
...@@ -198,20 +212,35 @@ impl Host {...@@ -198,20 +212,35 @@ impl Host {
198 let removed = self.members.access.lock().unwrap().remove(secret);212 let removed = self.members.access.lock().unwrap().remove(secret);
199 if let Some(removed) = removed {213 if let Some(removed) = removed {
200 for peer in removed.peers() {214 for peer in removed.peers() {
201 self.members.served.forget(&peer.hello.peer);215 self.members.served.forget(&peer.hello.peer, None);
202 }216 }
203 removed.leave("removed");217 removed.leave("removed");
204 }218 }
205 self.members.presence(next.secret)?;219 self.members.presence(next.secret)?;
206 *self.pairing.lock().unwrap() = Some(self.members.pair(&next)?);220 *self.pairing.lock().unwrap() = Some(self.members.pair(&next)?);
207 let access = self.members.access.lock().unwrap();221 let welcomes = {
208 for device in &next.members {222 let access = self.members.access.lock().unwrap();
209 if let Some(live) = access.get(&device.secret) {223 next.members
210 live.sender().send(224 .iter()
211 kind::WELCOME,225 .filter_map(|device| {
212 &self.members.welcome(&next, device.secret),226 let live = access.get(&device.secret)?;
213 None,227 let lines = live
214 );228 .peers()
229 .into_iter()
230 .filter_map(|peer| live.line(&peer.hello.peer))
231 .collect::<Vec<_>>();
232 Some((self.members.welcome(&next, device.secret), lines))
233 })
234 .collect::<Vec<_>>()
235 };
236 #[cfg(target_arch = "wasm32")]
237 drop(_change);
238 'welcomes: for (welcome, lines) in welcomes {
239 for line in lines {
240 if self.members.sharing.lock().unwrap().secret != welcome.room {
241 break 'welcomes;
242 }
243 let _ = line.send(kind::WELCOME, &welcome);
215 }244 }
216 }245 }
217 (self.members.events)();246 (self.members.events)();
...@@ -274,7 +303,7 @@ impl Members {...@@ -274,7 +303,7 @@ impl Members {
274303
275 fn open(self: &Arc<Self>, secret: [u8; 16]) -> io::Result<()> {304 fn open(self: &Arc<Self>, secret: [u8; 16]) -> io::Result<()> {
276 let members = Arc::downgrade(self);305 let members = Arc::downgrade(self);
277 let live = Live::start(306 let live = Live::start_with(
278 Hello {307 Hello {
279 serves: Some(self.sharing.lock().unwrap().share),308 serves: Some(self.sharing.lock().unwrap().share),
280 ..self.me.clone()309 ..self.me.clone()
...@@ -282,25 +311,42 @@ impl Members {...@@ -282,25 +311,42 @@ impl Members {
282 &Room::Notebook(secret),311 &Room::Notebook(secret),
283 self.reach,312 self.reach,
284 self.relay.as_deref(),313 self.relay.as_deref(),
285 move |event| {314 move |source, event| {
286 let Some(members) = members.upgrade() else {315 let Some(members) = members.upgrade() else {
287 return;316 return;
288 };317 };
289 match event {318 match event {
290 Event::Met(hello, line) => {319 Event::Met(hello, line) => {
291 let sharing = members.sharing.lock().unwrap();320 let sharing = members.sharing.lock().unwrap();
292 if !sharing.members.iter().any(|device| device.secret == secret) {321 let admitted =
293 line.hang_up("removed");322 if sharing.members.iter().any(|device| device.secret == secret) {
294 return;323 Ok(members
324 .served
325 .admit(source, hello, line)
326 .then(|| members.welcome(&sharing, secret)))
327 } else {
328 Err(())
329 };
330 // Browser send failures may reenter membership callbacks.
331 #[cfg(target_arch = "wasm32")]
332 drop(sharing);
333 match admitted {
334 Ok(Some(welcome)) => {
335 let _ = line.send(kind::WELCOME, &welcome);
336 }
337 Err(()) => {
338 #[cfg(not(target_arch = "wasm32"))]
339 drop(sharing);
340 line.hang_up("removed");
341 }
342 Ok(None) => {}
295 }343 }
296 members.served.admit(hello.peer, line);
297 let _ = line.send(kind::WELCOME, &members.welcome(&sharing, secret));
298 }344 }
299 Event::Left(hello) => members.served.forget(&hello.peer),345 Event::Left(hello) => members.served.forget(&hello.peer, Some((source, hello))),
300 Event::Frame { from, kind, body }346 Event::Frame { from, kind, body }
301 if wire::KNOWN.contains(&kind) && kind > 256 && kind != kind::REPLY =>347 if wire::KNOWN.contains(&kind) && kind > 256 && kind != kind::REPLY =>
302 {348 {
303 members.served.queue(&from.peer, kind, body)349 members.served.queue(source, from, kind, body)
304 }350 }
305 Event::Changed => (members.events)(),351 Event::Changed => (members.events)(),
306 _ => {}352 _ => {}
...@@ -349,7 +395,7 @@ impl Members {...@@ -349,7 +395,7 @@ impl Members {
349395
350 fn pair(self: &Arc<Self>, sharing: &Sharing) -> io::Result<Live> {396 fn pair(self: &Arc<Self>, sharing: &Sharing) -> io::Result<Live> {
351 let members = Arc::downgrade(self);397 let members = Arc::downgrade(self);
352 Live::start(398 Live::start_with(
353 Hello {399 Hello {
354 serves: None,400 serves: None,
355 ..self.me.clone()401 ..self.me.clone()
...@@ -357,21 +403,41 @@ impl Members {...@@ -357,21 +403,41 @@ impl Members {
357 &Room::share(&sharing.code, &sharing.password),403 &Room::share(&sharing.code, &sharing.password),
358 self.reach,404 self.reach,
359 self.relay.as_deref(),405 self.relay.as_deref(),
360 move |event| {406 move |source, event| {
361 let Some(members) = members.upgrade() else {407 let Some(members) = members.upgrade() else {
362 return;408 return;
363 };409 };
364 match event {410 match event {
365 Event::Met(hello, line) => {411 Event::Met(hello, line) => {
366 if members.sharing.lock().unwrap().approve {412 let approve = members.sharing.lock().unwrap().approve;
413 let (approval, retired) = {
367 let mut pending = members.pending.lock().unwrap();414 let mut pending = members.pending.lock().unwrap();
368 if pending.len() < 32 {415 if !source.connected(hello) {
369 pending.insert(hello.peer, (Arc::clone(hello), line.clone()));416 return;
370 let _ = line.send(kind::APPROVAL, &wire::Approval::Pending);417 }
418 if approve {
419 if pending.len() < 32 || pending.contains_key(&hello.peer) {
420 (
421 Some(wire::Approval::Pending),
422 pending.insert(
423 hello.peer,
424 (
425 Arc::clone(hello),
426 line.clone(),
427 Arc::downgrade(source),
428 ),
429 ),
430 )
431 } else {
432 (Some(wire::Approval::Failed), None)
433 }
371 } else {434 } else {
372 let _ = line.send(kind::APPROVAL, &wire::Approval::Failed);435 (None, None)
373 }436 }
374 drop(pending);437 };
438 drop(retired);
439 if let Some(approval) = approval {
440 let _ = line.send(kind::APPROVAL, &approval);
375 (members.events)();441 (members.events)();
376 } else if let Err(error) = members.grant(hello, line) {442 } else if let Err(error) = members.grant(hello, line) {
377 eprintln!("Live Share: could not admit a device: {error}");443 eprintln!("Live Share: could not admit a device: {error}");
...@@ -379,7 +445,19 @@ impl Members {...@@ -379,7 +445,19 @@ impl Members {
379 }445 }
380 }446 }
381 Event::Left(hello) => {447 Event::Left(hello) => {
382 members.pending.lock().unwrap().remove(&hello.peer);448 let retired = {
449 let mut pending = members.pending.lock().unwrap();
450 if source.gone(hello)
451 && pending
452 .get(&hello.peer)
453 .is_some_and(|(admitted, _, _)| Arc::ptr_eq(admitted, hello))
454 {
455 pending.remove(&hello.peer)
456 } else {
457 None
458 }
459 };
460 drop(retired);
383 (members.events)();461 (members.events)();
384 }462 }
385 Event::Changed => (members.events)(),463 Event::Changed => (members.events)(),
...@@ -389,3 +467,153 @@ impl Members {...@@ -389,3 +467,153 @@ impl Members {
389 )467 )
390 }468 }
391}469}
470
471#[cfg(all(test, not(target_arch = "wasm32")))]
472mod handover_tests {
473 use super::*;
474 use std::sync::atomic::AtomicUsize;
475
476 #[test]
477 fn handover_pair_pending_keeps_promoted_identity_and_rejects_retired_allow() {
478 let directory = tempfile::tempdir().unwrap();
479 let folder = directory.path().join("notebook");
480 std::fs::create_dir(&folder).unwrap();
481 let sharing = Sharing {
482 approve: true,
483 code: super::super::super::code::format(701, "ABCDEF").unwrap(),
484 ..Sharing::new("").unwrap()
485 };
486 let room = Room::share(&sharing.code, &sharing.password);
487 let host = Host::start(
488 crate::session::Notebook::open(&folder, directory.path().join("cache"))
489 .unwrap()
490 .into_storage(),
491 Hello::new("Notebook owner".into(), None).unwrap(),
492 sharing,
493 "Fixture",
494 None,
495 None,
496 || {},
497 |_| Ok(()),
498 )
499 .unwrap();
500 let callback = Arc::clone(&host.pairing.lock().unwrap().as_ref().unwrap().shared);
501 let valid = super::super::handover_tests::promoted_routes(
502 room,
503 |source, old, old_line| {
504 (callback.events)(source, Event::Met(old, old_line));
505 },
506 |source, old, old_line, new, new_line| {
507 host.allow(&old.peer).unwrap();
508 let no_grant = host.sharing().members.is_empty();
509 (callback.events)(source, Event::Met(new, new_line));
510 (callback.events)(source, Event::Met(old, old_line));
511 (callback.events)(source, Event::Left(old));
512 let kept = host
513 .requests()
514 .first()
515 .is_some_and(|request| Arc::ptr_eq(request, new));
516 host.stop();
517 kept && no_grant
518 },
519 );
520 assert!(
521 valid,
522 "pairing admitted a retired route or retired Left removed the promoted request"
523 );
524 }
525
526 #[test]
527 fn handover_pair_retired_met_cannot_persist_another_credential() {
528 let directory = tempfile::tempdir().unwrap();
529 let folder = directory.path().join("notebook");
530 std::fs::create_dir(&folder).unwrap();
531 let sharing = Sharing {
532 approve: false,
533 code: super::super::super::code::format(702, "ABCDEF").unwrap(),
534 ..Sharing::new("").unwrap()
535 };
536 let room = Room::share(&sharing.code, &sharing.password);
537 let kept = Arc::new(AtomicUsize::new(0));
538 let count = Arc::clone(&kept);
539 let host = Host::start(
540 crate::session::Notebook::open(&folder, directory.path().join("cache"))
541 .unwrap()
542 .into_storage(),
543 Hello::new("Notebook owner".into(), None).unwrap(),
544 sharing,
545 "Fixture",
546 None,
547 None,
548 || {},
549 move |_| {
550 count.fetch_add(1, Ordering::SeqCst);
551 Ok(())
552 },
553 )
554 .unwrap();
555 let callback = Arc::clone(&host.pairing.lock().unwrap().as_ref().unwrap().shared);
556 let valid = super::super::handover_tests::promoted_routes(
557 room,
558 |_, _, _| {},
559 |source, old, old_line, new, new_line| {
560 (callback.events)(source, Event::Met(new, new_line));
561 let after_current = kept.load(Ordering::SeqCst);
562 (callback.events)(source, Event::Met(old, old_line));
563 let refused = kept.load(Ordering::SeqCst) == after_current
564 && after_current == 2
565 && host.sharing().members.len() == 1;
566 host.stop();
567 refused
568 },
569 );
570 assert!(valid, "retired pairing Met persisted another credential");
571 }
572
573 #[test]
574 fn handover_retired_membership_rejects_deferred_access_met() {
575 let directory = tempfile::tempdir().unwrap();
576 let folder = directory.path().join("notebook");
577 std::fs::create_dir(&folder).unwrap();
578 let secret = [71; 16];
579 let sharing = Sharing {
580 code: super::super::super::code::format(703, "ABCDEF").unwrap(),
581 members: vec![Device {
582 secret,
583 name: "Retired device".into(),
584 device: None,
585 }],
586 ..Sharing::new("").unwrap()
587 };
588 let host = Host::start(
589 crate::session::Notebook::open(&folder, directory.path().join("cache"))
590 .unwrap()
591 .into_storage(),
592 Hello::new("Notebook owner".into(), None).unwrap(),
593 sharing,
594 "Fixture",
595 None,
596 None,
597 || {},
598 |_| Ok(()),
599 )
600 .unwrap();
601 let callback = Arc::clone(&host.members.access.lock().unwrap()[&secret].shared);
602 let refused = super::super::handover_tests::promoted_routes(
603 Room::Notebook(secret),
604 |_, _, _| {},
605 |source, _, _, current, line| {
606 host.remove(&secret).unwrap();
607 (callback.events)(source, Event::Met(current, line));
608 let refused = host.members.served.guests.lock().unwrap().is_empty()
609 && host.sharing().members.is_empty();
610 host.stop();
611 refused
612 },
613 );
614 assert!(
615 refused,
616 "deferred access callback readmitted a removed device"
617 );
618 }
619}
crates/notebook/src/live/web.rs+80-35
...@@ -42,7 +42,8 @@ struct Shared {...@@ -42,7 +42,8 @@ struct Shared {
42 me: Hello,42 me: Hello,
43 room: Room,43 room: Room,
44 url: String,44 url: String,
45 events: Box<dyn Fn(Event) + Send + Sync>,45 #[allow(clippy::type_complexity)]
46 events: Box<dyn Fn(&Arc<Shared>, Event) + Send + Sync>,
46 stopped: AtomicBool,47 stopped: AtomicBool,
47 state: Mutex<State>,48 state: Mutex<State>,
48}49}
...@@ -139,9 +140,19 @@ impl Live {...@@ -139,9 +140,19 @@ impl Live {
139 pub fn start(140 pub fn start(
140 me: Hello,141 me: Hello,
141 room: &Room,142 room: &Room,
142 _: Option<Reach>,143 reach: Option<Reach>,
143 relay: Option<&str>,144 relay: Option<&str>,
144 events: impl Fn(Event) + Send + Sync + 'static,145 events: impl Fn(Event) + Send + Sync + 'static,
146 ) -> io::Result<Self> {
147 Self::start_with(me, room, reach, relay, move |_, event| events(event))
148 }
149
150 fn start_with(
151 me: Hello,
152 room: &Room,
153 _: Option<Reach>,
154 relay: Option<&str>,
155 events: impl Fn(&Arc<Shared>, Event) + Send + Sync + 'static,
145 ) -> io::Result<Self> {156 ) -> io::Result<Self> {
146 let tag = room.tag().ok_or(io::ErrorKind::InvalidInput)?;157 let tag = room.tag().ok_or(io::ErrorKind::InvalidInput)?;
147 let relay = relay158 let relay = relay
...@@ -223,17 +234,7 @@ impl Live {...@@ -223,17 +234,7 @@ impl Live {
223 Sender(Arc::downgrade(&self.0))234 Sender(Arc::downgrade(&self.0))
224 }235 }
225 pub fn peers(&self) -> Vec<Peer> {236 pub fn peers(&self) -> Vec<Peer> {
226 let state = self.0.state.lock().unwrap();237 self.0.peers()
227 let mut peers = BTreeMap::new();
228 for peer in state
229 .members
230 .values()
231 .filter_map(|m| m.peer.as_ref())
232 .chain(state.streams.values().filter_map(|s| s.peer.as_ref()))
233 {
234 peers.insert(peer.hello.peer, peer.clone());
235 }
236 peers.into_values().collect()
237 }238 }
238 pub fn line(&self, peer: &[u8; 16]) -> Option<Line> {239 pub fn line(&self, peer: &[u8; 16]) -> Option<Line> {
239 self.0240 self.0
...@@ -288,11 +289,45 @@ impl Live {...@@ -288,11 +289,45 @@ impl Live {
288impl Drop for Live {289impl Drop for Live {
289 fn drop(&mut self) {290 fn drop(&mut self) {
290 self.0.stopped.store(true, Ordering::Release);291 self.0.stopped.store(true, Ordering::Release);
291 close(self.0.state.lock().unwrap().socket);292 let socket = self.0.state.lock().unwrap().socket;
293 close(socket);
292 }294 }
293}295}
294296
295impl Shared {297impl Shared {
298 fn peers(&self) -> Vec<Peer> {
299 let state = self.state.lock().unwrap();
300 let mut peers = BTreeMap::new();
301 for peer in state
302 .members
303 .values()
304 .filter_map(|m| m.peer.as_ref())
305 .chain(state.streams.values().filter_map(|s| s.peer.as_ref()))
306 {
307 peers.insert(peer.hello.peer, peer.clone());
308 }
309 peers.into_values().collect()
310 }
311
312 fn gone(&self, hello: &Arc<Hello>) -> bool {
313 !self.state.lock().unwrap().streams.values().any(|stream| {
314 stream
315 .peer
316 .as_ref()
317 .is_some_and(|peer| Arc::ptr_eq(&peer.hello, hello))
318 })
319 }
320
321 fn connected(&self, hello: &Arc<Hello>) -> bool {
322 !self.stopped.load(Ordering::Acquire)
323 && self.state.lock().unwrap().streams.values().any(|stream| {
324 stream
325 .peer
326 .as_ref()
327 .is_some_and(|peer| Arc::ptr_eq(&peer.hello, hello))
328 })
329 }
330
296 fn connect(self: &Arc<Self>) -> io::Result<()> {331 fn connect(self: &Arc<Self>) -> io::Result<()> {
297 let weak = Arc::downgrade(self);332 let weak = Arc::downgrade(self);
298 let message = Closure::<dyn FnMut(JsValue)>::new(move |data: JsValue| {333 let message = Closure::<dyn FnMut(JsValue)>::new(move |data: JsValue| {
...@@ -324,13 +359,12 @@ impl Shared {...@@ -324,13 +359,12 @@ impl Shared {
324 }359 }
325360
326 fn disconnected(self: &Arc<Self>) {361 fn disconnected(self: &Arc<Self>) {
327 let peers = {362 let (socket, peers) = {
328 let mut state = self.state.lock().unwrap();363 let mut state = self.state.lock().unwrap();
329 let socket = std::mem::replace(&mut state.socket, 0);364 let socket = std::mem::replace(&mut state.socket, 0);
330 if socket == 0 {365 if socket == 0 {
331 return;366 return;
332 }367 }
333 close(socket);
334 let peers: Vec<_> = state368 let peers: Vec<_> = state
335 .streams369 .streams
336 .values()370 .values()
...@@ -343,12 +377,13 @@ impl Shared {...@@ -343,12 +377,13 @@ impl Shared {
343 state.members.clear();377 state.members.clear();
344 state.sealer = None;378 state.sealer = None;
345 state.relayed = Relayed::Unreachable(Trouble::Other);379 state.relayed = Relayed::Unreachable(Trouble::Other);
346 peers380 (socket, peers)
347 };381 };
382 close(socket);
348 for hello in peers {383 for hello in peers {
349 (self.events)(Event::Left(&hello));384 (self.events)(self, Event::Left(&hello));
350 }385 }
351 (self.events)(Event::Changed);386 (self.events)(self, Event::Changed);
352 let weak = Arc::downgrade(self);387 let weak = Arc::downgrade(self);
353 let _ = crate::task::spawn("live reconnect", move || async move {388 let _ = crate::task::spawn("live reconnect", move || async move {
354 let (_, wait) = crate::task::channel();389 let (_, wait) = crate::task::channel();
...@@ -464,13 +499,13 @@ impl Shared {...@@ -464,13 +499,13 @@ impl Shared {
464 })499 })
465 };500 };
466 if let Some(hello) = hello {501 if let Some(hello) = hello {
467 (self.events)(Event::Left(&hello));502 (self.events)(self, Event::Left(&hello));
468 }503 }
469 }504 }
470 ::relay::Notice::Burned => self.state.lock().unwrap().burned = true,505 ::relay::Notice::Burned => self.state.lock().unwrap().burned = true,
471 _ => {}506 _ => {}
472 }507 }
473 (self.events)(Event::Changed);508 (self.events)(self, Event::Changed);
474 return Ok(());509 return Ok(());
475 }510 }
476 let bytes = js_sys::Uint8Array::new(&data).to_vec();511 let bytes = js_sys::Uint8Array::new(&data).to_vec();
...@@ -523,7 +558,7 @@ impl Shared {...@@ -523,7 +558,7 @@ impl Shared {
523 if serves {558 if serves {
524 self.meet(slot)?;559 self.meet(slot)?;
525 }560 }
526 (self.events)(Event::Changed);561 (self.events)(self, Event::Changed);
527 }562 }
528 kind::PRESENCE => {563 kind::PRESENCE => {
529 let presence = minicbor::decode(&body).map_err(|_| io::ErrorKind::InvalidData)?;564 let presence = minicbor::decode(&body).map_err(|_| io::ErrorKind::InvalidData)?;
...@@ -537,15 +572,18 @@ impl Shared {...@@ -537,15 +572,18 @@ impl Shared {
537 {572 {
538 peer.presence = Some(presence);573 peer.presence = Some(presence);
539 }574 }
540 (self.events)(Event::Changed);575 (self.events)(self, Event::Changed);
541 }576 }
542 kind if kind < 256 => {577 kind if kind < 256 => {
543 if let Some(hello) = hello {578 if let Some(hello) = hello {
544 (self.events)(Event::Frame {579 (self.events)(
545 from: &hello,580 self,
546 kind,581 Event::Frame {
547 body: &body,582 from: &hello,
548 });583 kind,
584 body: &body,
585 },
586 );
549 }587 }
550 }588 }
551 _ => {}589 _ => {}
...@@ -568,7 +606,9 @@ impl Shared {...@@ -568,7 +606,9 @@ impl Shared {
568 loop {606 loop {
569 let event = {607 let event = {
570 let mut state = self.state.lock().unwrap();608 let mut state = self.state.lock().unwrap();
571 let stream = state.streams.get_mut(&slot).unwrap();609 let Some(stream) = state.streams.get_mut(&slot) else {
610 break;
611 };
572 let Some(length) = stream.bytes.get(..4) else {612 let Some(length) = stream.bytes.get(..4) else {
573 break;613 break;
574 };614 };
...@@ -613,12 +653,17 @@ impl Shared {...@@ -613,12 +653,17 @@ impl Shared {
613 if let Some((line, hello, kind, body)) = event {653 if let Some((line, hello, kind, body)) = event {
614 match hello {654 match hello {
615 None => line.send(kind::HELLO, &self.me)?,655 None => line.send(kind::HELLO, &self.me)?,
616 Some(hello) if kind == kind::HELLO => (self.events)(Event::Met(&hello, &line)),656 Some(hello) if kind == kind::HELLO => {
617 Some(hello) => (self.events)(Event::Frame {657 (self.events)(self, Event::Met(&hello, &line))
618 from: &hello,658 }
619 kind,659 Some(hello) => (self.events)(
620 body: &body,660 self,
621 }),661 Event::Frame {
662 from: &hello,
663 kind,
664 body: &body,
665 },
666 ),
622 }667 }
623 }668 }
624 }669 }
crates/notebook/tests/live_membership.rs+5-1
...@@ -104,6 +104,9 @@ fn removal_retires_one_credential_and_other_devices_reconnect_after_a_restart()...@@ -104,6 +104,9 @@ fn removal_retires_one_credential_and_other_devices_reconnect_after_a_restart()
104 &directory.path().join("alice"),104 &directory.path().join("alice"),
105 );105 );
106 let (bob, _) = guest("Bob", &original_code, &url, &directory.path().join("bob"));106 let (bob, _) = guest("Bob", &original_code, &url, &directory.path().join("bob"));
107 until("the initial presence room never met both devices", || {
108 host.guests().len() == 2
109 });
107 let before = host.sharing();110 let before = host.sharing();
108 let alice_key = before111 let alice_key = before
109 .members112 .members
...@@ -122,7 +125,8 @@ fn removal_retires_one_credential_and_other_devices_reconnect_after_a_restart()...@@ -122,7 +125,8 @@ fn removal_retires_one_credential_and_other_devices_reconnect_after_a_restart()
122 bob.ended() == Some(share::Ended::Removed) && bob.host().is_none()125 bob.ended() == Some(share::Ended::Removed) && bob.host().is_none()
123 });126 });
124 until("presence did not move to the new room", || {127 until("presence did not move to the new room", || {
125 host.guests().len() == 1128 let guests = host.guests();
129 guests.len() == 1 && guests[0].hello.name == "Alice"
126 });130 });
127 let current = host.sharing();131 let current = host.sharing();
128 assert_ne!(before.secret, current.secret);132 assert_ne!(before.secret, current.secret);