| author | |
| committer | |
| log | 725072f4398ea34fd120598c30e2c7c90714ffd3 |
| tree | 48448889f0b2f9ee899bfe71e5c347e88412b824 |
| parent | 55d5656dafafcc0610b573a1d68d6051fa92c260 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
Give each admitted device its own persisted storage credential, and rotate the
presence key and invite code on removal. Persist grants before sending credentials
and reject queued requests after withdrawal. Show pending requests and connected
or offline devices in Live Share; joining can wait for approval or be cancelled.
Raise relay address limits to accommodate the private device rooms behind a
shared network. Full CI, admission and revocation tests, native UI replay, and a
30-guest typing run passed.
Assisted-by: gpt-6.1-sol14 files changed, 1007 insertions(+), 264 deletions(-)
crates/notebook/examples/live_crowd.rs+1| ... | @@ -133,6 +133,7 @@ fn host(url: &str, paragraphs: usize) { | ... | @@ -133,6 +133,7 @@ fn host(url: &str, paragraphs: usize) { |
| 133 | None, | 133 | None, |
| 134 | Some(url), | 134 | Some(url), |
| 135 | || {}, | 135 | || {}, |
| 136 | |_| Ok(()), | ||
| 136 | ) | 137 | ) |
| 137 | .unwrap(); | 138 | .unwrap(); |
| 138 | until("no code", Duration::from_secs(30), || { | 139 | until("no code", Duration::from_secs(30), || { |
crates/notebook/examples/live_latency.rs+1| ... | @@ -160,6 +160,7 @@ fn main() { | ... | @@ -160,6 +160,7 @@ fn main() { |
| 160 | reach, | 160 | reach, |
| 161 | relay, | 161 | relay, |
| 162 | || {}, | 162 | || {}, |
| 163 | |_| Ok(()), | ||
| 163 | ) | 164 | ) |
| 164 | .unwrap(); | 165 | .unwrap(); |
| 165 | until("no code", || { | 166 | until("no code", || { |
crates/notebook/src/live/share.rs+187-216| ... | @@ -1,9 +1,9 @@ | ... | @@ -1,9 +1,9 @@ |
| 1 | //! Live Share: a notebook one Snowbound holds, opened on others through a short code. The | 1 | //! Live Share: a notebook one Snowbound holds, opened on others through a short code. The |
| 2 | //! host serves its notebook's storage verbs (`session::Storage`) and batches guests' ops into | 2 | //! host serves its notebook's storage verbs (`session::Storage`) and batches guests' ops into |
| 3 | //! guarded publications. A guest runs the same replica and durable queue as on an SMB share; | 3 | //! guarded publications. A guest runs the same replica and durable queue as on an SMB share; |
| 4 | //! protected sections and older peers use transactions. A guest first meets the host in the code's room, where | 4 | //! protected sections use transactions. A guest first meets the host in the code's room, |
| 5 | //! the host welcomes it with the share's room and secret; a new share has a new secret, so | 5 | //! where approval grants a device's own access credential. Presence has a separate room |
| 6 | //! stopping retires every guest. Large bodies travel a chunk at a time, each answered before | 6 | //! whose key changes when a device is removed. Large bodies travel a chunk at a time, each answered before |
| 7 | //! the next, so a relay never holds much for a slow peer. | 7 | //! the next, so a relay never holds much for a slow peer. |
| 8 | 8 | ||
| 9 | use super::{ | 9 | use super::{ |
| ... | @@ -20,7 +20,7 @@ use std::{ | ... | @@ -20,7 +20,7 @@ use std::{ |
| 20 | io, | 20 | io, |
| 21 | path::PathBuf, | 21 | path::PathBuf, |
| 22 | sync::{ | 22 | sync::{ |
| 23 | Arc, Condvar, Mutex, OnceLock, | 23 | Arc, Condvar, Mutex, |
| 24 | atomic::{AtomicBool, AtomicU64, Ordering}, | 24 | atomic::{AtomicBool, AtomicU64, Ordering}, |
| 25 | mpsc, | 25 | mpsc, |
| 26 | }, | 26 | }, |
| ... | @@ -29,6 +29,8 @@ use std::{ | ... | @@ -29,6 +29,8 @@ use std::{ |
| 29 | }; | 29 | }; |
| 30 | 30 | ||
| 31 | mod batch; | 31 | mod batch; |
| 32 | mod membership; | ||
| 33 | pub use membership::Host; | ||
| 32 | 34 | ||
| 33 | /// The most bytes one message of a read or an upload carries. | 35 | /// The most bytes one message of a read or an upload carries. |
| 34 | const CHUNK: usize = 128 << 10; | 36 | const CHUNK: usize = 128 << 10; |
| ... | @@ -77,11 +79,22 @@ pub fn code(typed: &str) -> Option<String> { | ... | @@ -77,11 +79,22 @@ pub fn code(typed: &str) -> Option<String> { |
| 77 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] | 79 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] |
| 78 | pub struct Sharing { | 80 | pub struct Sharing { |
| 79 | pub share: [u8; 16], | 81 | pub share: [u8; 16], |
| 80 | /// The share room's secret, which every guest welcomed holds. | 82 | /// The presence key, rotated when a device is removed. |
| 81 | pub secret: [u8; 16], | 83 | pub secret: [u8; 16], |
| 82 | /// The code, or its secret alone until it has a number (`super::code`). | 84 | /// The code, or its secret alone until it has a number (`super::code`). |
| 83 | pub code: String, | 85 | pub code: String, |
| 84 | pub password: String, | 86 | pub password: String, |
| 87 | #[serde(default)] | ||
| 88 | pub approve: bool, | ||
| 89 | #[serde(default)] | ||
| 90 | pub members: Vec<Device>, | ||
| 91 | } | ||
| 92 | |||
| 93 | #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] | ||
| 94 | pub struct Device { | ||
| 95 | pub secret: [u8; 16], | ||
| 96 | pub name: String, | ||
| 97 | pub device: Option<String>, | ||
| 85 | } | 98 | } |
| 86 | 99 | ||
| 87 | impl Sharing { | 100 | impl Sharing { |
| ... | @@ -95,6 +108,8 @@ impl Sharing { | ... | @@ -95,6 +108,8 @@ impl Sharing { |
| 95 | secret: random[16..].try_into().expect("16 bytes"), | 108 | secret: random[16..].try_into().expect("16 bytes"), |
| 96 | code: super::code::secret()?, | 109 | code: super::code::secret()?, |
| 97 | password: password.to_owned(), | 110 | password: password.to_owned(), |
| 111 | approve: false, | ||
| 112 | members: Vec::new(), | ||
| 98 | }) | 113 | }) |
| 99 | } | 114 | } |
| 100 | } | 115 | } |
| ... | @@ -111,7 +126,9 @@ pub enum Refusal { | ... | @@ -111,7 +126,9 @@ pub enum Refusal { |
| 111 | Malformed, | 126 | Malformed, |
| 112 | /// The person sharing runs a Snowbound of another Live Share version: the newer one's | 127 | /// The person sharing runs a Snowbound of another Live Share version: the newer one's |
| 113 | /// `true` where it is theirs, so this one should update. | 128 | /// `true` where it is theirs, so this one should update. |
| 114 | Version { theirs_newer: bool }, | 129 | Version { |
| 130 | theirs_newer: bool, | ||
| 131 | }, | ||
| 115 | /// The code's secret or password is wrong. | 132 | /// The code's secret or password is wrong. |
| 116 | Wrong, | 133 | Wrong, |
| 117 | /// No one shares with the code's number now. | 134 | /// No one shares with the code's number now. |
| ... | @@ -126,6 +143,9 @@ pub enum Refusal { | ... | @@ -126,6 +143,9 @@ pub enum Refusal { |
| 126 | Unreachable(super::Trouble), | 143 | Unreachable(super::Trouble), |
| 127 | /// The relay let this end in, but no one answered. | 144 | /// The relay let this end in, but no one answered. |
| 128 | TimedOut, | 145 | TimedOut, |
| 146 | Declined, | ||
| 147 | Cancelled, | ||
| 148 | NotAdmitted, | ||
| 129 | } | 149 | } |
| 130 | 150 | ||
| 131 | /// Meets the host sharing `code` (and `password`) as `me`, on the networks `reach` names and | 151 | /// Meets the host sharing `code` (and `password`) as `me`, on the networks `reach` names and |
| ... | @@ -136,9 +156,23 @@ pub fn join( | ... | @@ -136,9 +156,23 @@ pub fn join( |
| 136 | password: &str, | 156 | password: &str, |
| 137 | reach: Option<Reach>, | 157 | reach: Option<Reach>, |
| 138 | relay: Option<&str>, | 158 | relay: Option<&str>, |
| 159 | ) -> std::result::Result<Welcome, Refusal> { | ||
| 160 | join_while(me, code, password, reach, relay, |_| true) | ||
| 161 | } | ||
| 162 | |||
| 163 | /// Joins while `waiting` returns true, reporting whether the host is deciding approval. | ||
| 164 | pub fn join_while( | ||
| 165 | me: Hello, | ||
| 166 | code: &str, | ||
| 167 | password: &str, | ||
| 168 | reach: Option<Reach>, | ||
| 169 | relay: Option<&str>, | ||
| 170 | continue_joining: impl Fn(bool) -> bool, | ||
| 139 | ) -> std::result::Result<Welcome, Refusal> { | 171 | ) -> std::result::Result<Welcome, Refusal> { |
| 140 | let code = self::code(code).ok_or(Refusal::Malformed)?; | 172 | let code = self::code(code).ok_or(Refusal::Malformed)?; |
| 141 | let (welcomed, welcome) = mpsc::channel(); | 173 | let (welcomed, welcome) = mpsc::channel(); |
| 174 | let approving = Arc::new(AtomicBool::new(false)); | ||
| 175 | let approval = Arc::clone(&approving); | ||
| 142 | let (changed, waiting) = mpsc::channel(); | 176 | let (changed, waiting) = mpsc::channel(); |
| 143 | let live = Live::start( | 177 | let live = Live::start( |
| 144 | me, | 178 | me, |
| ... | @@ -152,7 +186,24 @@ pub fn join( | ... | @@ -152,7 +186,24 @@ pub fn join( |
| 152 | .. | 186 | .. |
| 153 | } => { | 187 | } => { |
| 154 | if let Ok(body) = minicbor::decode::<Welcome>(body) { | 188 | if let Ok(body) = minicbor::decode::<Welcome>(body) { |
| 155 | let _ = welcomed.send(body); | 189 | let _ = welcomed.send(Ok(body)); |
| 190 | } | ||
| 191 | } | ||
| 192 | Event::Frame { | ||
| 193 | kind: kind::APPROVAL, | ||
| 194 | body, | ||
| 195 | .. | ||
| 196 | } => { | ||
| 197 | if let Ok(body) = minicbor::decode::<wire::Approval>(body) { | ||
| 198 | match body { | ||
| 199 | wire::Approval::Pending => approval.store(true, Ordering::Release), | ||
| 200 | wire::Approval::Declined => { | ||
| 201 | let _ = welcomed.send(Err(Refusal::Declined)); | ||
| 202 | } | ||
| 203 | wire::Approval::Failed => { | ||
| 204 | let _ = welcomed.send(Err(Refusal::NotAdmitted)); | ||
| 205 | } | ||
| 206 | } | ||
| 156 | } | 207 | } |
| 157 | } | 208 | } |
| 158 | _ => { | 209 | _ => { |
| ... | @@ -163,8 +214,11 @@ pub fn join( | ... | @@ -163,8 +214,11 @@ pub fn join( |
| 163 | .map_err(|_| Refusal::Unreachable(super::Trouble::Other))?; | 214 | .map_err(|_| Refusal::Unreachable(super::Trouble::Other))?; |
| 164 | let start = Instant::now(); | 215 | let start = Instant::now(); |
| 165 | loop { | 216 | loop { |
| 217 | if !continue_joining(approving.load(Ordering::Acquire)) { | ||
| 218 | return Err(Refusal::Cancelled); | ||
| 219 | } | ||
| 166 | if let Ok(welcome) = welcome.try_recv() { | 220 | if let Ok(welcome) = welcome.try_recv() { |
| 167 | return Ok(welcome); | 221 | return welcome; |
| 168 | } | 222 | } |
| 169 | if let Some(version) = live.other_version() { | 223 | if let Some(version) = live.other_version() { |
| 170 | return Err(Refusal::Version { | 224 | return Err(Refusal::Version { |
| ... | @@ -192,199 +246,21 @@ pub fn join( | ... | @@ -192,199 +246,21 @@ pub fn join( |
| 192 | Relayed::Unknown if relay.is_none() && waited > Duration::from_secs(10) => { | 246 | Relayed::Unknown if relay.is_none() && waited > Duration::from_secs(10) => { |
| 193 | return Err(Refusal::NoOne); | 247 | return Err(Refusal::NoOne); |
| 194 | } | 248 | } |
| 195 | _ if waited > Duration::from_secs(20) => return Err(Refusal::TimedOut), | 249 | _ if waited |
| 250 | > Duration::from_secs(if approving.load(Ordering::Acquire) { | ||
| 251 | 300 | ||
| 252 | } else { | ||
| 253 | 20 | ||
| 254 | }) => | ||
| 255 | { | ||
| 256 | return Err(Refusal::TimedOut); | ||
| 257 | } | ||
| 196 | _ => {} | 258 | _ => {} |
| 197 | } | 259 | } |
| 198 | let _ = waiting.recv_timeout(Duration::from_millis(100)); | 260 | let _ = waiting.recv_timeout(Duration::from_millis(100)); |
| 199 | } | 261 | } |
| 200 | } | 262 | } |
| 201 | 263 | ||
| 202 | /// A notebook shared while it lives: the share's room, serving the notebook's storage to the | ||
| 203 | /// guests in it, and the code's room, welcoming whoever knows the code. | ||
| 204 | pub struct Host { | ||
| 205 | me: Hello, | ||
| 206 | notebook: String, | ||
| 207 | reach: Option<Reach>, | ||
| 208 | relay: Option<String>, | ||
| 209 | sharing: Mutex<Sharing>, | ||
| 210 | /// The share's room and the code's, until it stops. | ||
| 211 | room: Mutex<Option<Live>>, | ||
| 212 | pairing: Mutex<Option<Live>>, | ||
| 213 | served: Arc<Served>, | ||
| 214 | events: Arc<dyn Fn() + Send + Sync>, | ||
| 215 | } | ||
| 216 | |||
| 217 | impl Host { | ||
| 218 | /// Shares `storage`, the notebook named `notebook`, as `sharing` says, as `me`, where | ||
| 219 | /// `reach` and `relay` say. `events` runs on a network thread whenever the guests or the | ||
| 220 | /// code change. What guests change reaches the host as its own watch on the notebook's | ||
| 221 | /// folder reports it, and reaches the other guests at once. | ||
| 222 | pub fn start( | ||
| 223 | storage: Box<dyn Storage>, | ||
| 224 | me: Hello, | ||
| 225 | sharing: Sharing, | ||
| 226 | notebook: &str, | ||
| 227 | reach: Option<Reach>, | ||
| 228 | relay: Option<&str>, | ||
| 229 | events: impl Fn() + Send + Sync + 'static, | ||
| 230 | ) -> io::Result<Self> { | ||
| 231 | let events: Arc<dyn Fn() + Send + Sync> = Arc::new(events); | ||
| 232 | let served = Arc::new(Served { | ||
| 233 | storage, | ||
| 234 | images: Mutex::default(), | ||
| 235 | snapshots: Mutex::default(), | ||
| 236 | puts: Mutex::default(), | ||
| 237 | guests: Mutex::default(), | ||
| 238 | writers: Mutex::default(), | ||
| 239 | host: Mutex::default(), | ||
| 240 | room: OnceLock::new(), | ||
| 241 | }); | ||
| 242 | let serving = Hello { | ||
| 243 | serves: Some(sharing.share), | ||
| 244 | ..me.clone() | ||
| 245 | }; | ||
| 246 | let (heard, told) = (Arc::clone(&served), Arc::clone(&events)); | ||
| 247 | let room = Live::start( | ||
| 248 | serving, | ||
| 249 | &Room::Notebook(sharing.secret), | ||
| 250 | reach, | ||
| 251 | relay, | ||
| 252 | move |event| match event { | ||
| 253 | Event::Met(hello, line) => heard.admit(hello.peer, line), | ||
| 254 | Event::Left(hello) => heard.forget(&hello.peer), | ||
| 255 | Event::Frame { | ||
| 256 | from, kind, body, .. | ||
| 257 | } if wire::KNOWN.contains(&kind) && kind > 256 && kind != kind::REPLY => { | ||
| 258 | heard.queue(&from.peer, kind, body); | ||
| 259 | } | ||
| 260 | Event::Changed => told(), | ||
| 261 | _ => {} | ||
| 262 | }, | ||
| 263 | )?; | ||
| 264 | let _ = served.room.set(room.sender()); | ||
| 265 | let host = Self { | ||
| 266 | notebook: notebook.to_owned(), | ||
| 267 | reach, | ||
| 268 | relay: relay.map(str::to_owned), | ||
| 269 | pairing: Mutex::new(Some(pair(&me, &sharing, notebook, reach, relay, &events)?)), | ||
| 270 | me, | ||
| 271 | sharing: Mutex::new(sharing), | ||
| 272 | room: Mutex::new(Some(room)), | ||
| 273 | served, | ||
| 274 | events, | ||
| 275 | }; | ||
| 276 | Ok(host) | ||
| 277 | } | ||
| 278 | |||
| 279 | /// The code guests type, once it has its number, and none once stopped. A code with too | ||
| 280 | /// many wrong tries is replaced by one with a new secret. | ||
| 281 | pub fn code(&self) -> Option<String> { | ||
| 282 | let mut pairing = self.pairing.lock().unwrap(); | ||
| 283 | let pairing = pairing.as_mut()?; | ||
| 284 | if pairing.burned() { | ||
| 285 | let mut sharing = self.sharing.lock().unwrap(); | ||
| 286 | sharing.code = super::code::secret().ok()?; | ||
| 287 | let relay = self.relay.as_deref(); | ||
| 288 | *pairing = pair( | ||
| 289 | &self.me, | ||
| 290 | &sharing, | ||
| 291 | &self.notebook, | ||
| 292 | self.reach, | ||
| 293 | relay, | ||
| 294 | &self.events, | ||
| 295 | ) | ||
| 296 | .ok()?; | ||
| 297 | } | ||
| 298 | let code = pairing.code(); | ||
| 299 | if let Some(code) = &code { | ||
| 300 | self.sharing.lock().unwrap().code = code.clone(); | ||
| 301 | } | ||
| 302 | code | ||
| 303 | } | ||
| 304 | |||
| 305 | /// The share as it stands, to take up again after a relaunch. | ||
| 306 | pub fn sharing(&self) -> Sharing { | ||
| 307 | self.code(); | ||
| 308 | self.sharing.lock().unwrap().clone() | ||
| 309 | } | ||
| 310 | |||
| 311 | /// How the relay last answered the code's room. | ||
| 312 | pub fn relayed(&self) -> Relayed { | ||
| 313 | let pairing = self.pairing.lock().unwrap(); | ||
| 314 | pairing.as_ref().map_or(Relayed::Unknown, Live::relayed) | ||
| 315 | } | ||
| 316 | |||
| 317 | /// The peers in the share's room. | ||
| 318 | pub fn guests(&self) -> Vec<Peer> { | ||
| 319 | let room = self.room.lock().unwrap(); | ||
| 320 | room.as_ref().map(Live::peers).unwrap_or_default() | ||
| 321 | } | ||
| 322 | |||
| 323 | pub fn set_presence(&self, presence: Presence) { | ||
| 324 | if let Some(room) = &*self.room.lock().unwrap() { | ||
| 325 | room.set_presence(presence); | ||
| 326 | } | ||
| 327 | } | ||
| 328 | |||
| 329 | /// Has `listener` hear the catalog paths guests change from now on, sooner than a watch | ||
| 330 | /// on the notebook's folder would. | ||
| 331 | pub fn on_changed(&self, listener: crate::session::Listener) { | ||
| 332 | *self.served.host.lock().unwrap() = Some(listener); | ||
| 333 | } | ||
| 334 | |||
| 335 | /// Tells every guest the files at these catalog paths changed, with what changed in the | ||
| 336 | /// sections a guest read lately. | ||
| 337 | pub fn touched(&self, paths: &[String]) { | ||
| 338 | for path in paths { | ||
| 339 | self.served.changed_here(path); | ||
| 340 | } | ||
| 341 | self.served.tell(paths); | ||
| 342 | } | ||
| 343 | |||
| 344 | /// Stops sharing: no one new is welcomed, and every guest hears so and is let go. | ||
| 345 | pub fn stop(&self) { | ||
| 346 | drop(self.pairing.lock().unwrap().take()); | ||
| 347 | let room = self.room.lock().unwrap().take(); | ||
| 348 | if let Some(room) = room { | ||
| 349 | room.leave(STOPPED); | ||
| 350 | } | ||
| 351 | } | ||
| 352 | } | ||
| 353 | |||
| 354 | /// The code's room, welcoming whoever knows the code to the share. | ||
| 355 | fn pair( | ||
| 356 | me: &Hello, | ||
| 357 | sharing: &Sharing, | ||
| 358 | notebook: &str, | ||
| 359 | reach: Option<Reach>, | ||
| 360 | relay: Option<&str>, | ||
| 361 | events: &Arc<dyn Fn() + Send + Sync>, | ||
| 362 | ) -> io::Result<Live> { | ||
| 363 | let welcome = Welcome { | ||
| 364 | share: sharing.share, | ||
| 365 | secret: sharing.secret, | ||
| 366 | notebook: notebook.to_owned(), | ||
| 367 | host: me.name.clone(), | ||
| 368 | }; | ||
| 369 | let told = Arc::clone(events); | ||
| 370 | Live::start( | ||
| 371 | Hello { | ||
| 372 | serves: None, | ||
| 373 | ..me.clone() | ||
| 374 | }, | ||
| 375 | &Room::share(&sharing.code, &sharing.password), | ||
| 376 | reach, | ||
| 377 | relay, | ||
| 378 | move |event| match event { | ||
| 379 | Event::Met(_, line) => { | ||
| 380 | let _ = line.send(kind::WELCOME, &welcome); | ||
| 381 | } | ||
| 382 | Event::Changed => told(), | ||
| 383 | _ => {} | ||
| 384 | }, | ||
| 385 | ) | ||
| 386 | } | ||
| 387 | |||
| 388 | /// A host's side of its guests' storage requests. | 264 | /// A host's side of its guests' storage requests. |
| 389 | struct Served { | 265 | struct Served { |
| 390 | storage: Box<dyn Storage>, | 266 | storage: Box<dyn Storage>, |
| ... | @@ -399,7 +275,7 @@ struct Served { | ... | @@ -399,7 +275,7 @@ struct Served { |
| 399 | /// Hears the paths guests changed, as the host's own notebook should. | 275 | /// Hears the paths guests changed, as the host's own notebook should. |
| 400 | host: Mutex<Option<crate::session::Listener>>, | 276 | host: Mutex<Option<crate::session::Listener>>, |
| 401 | /// The share's room, to tell guests what changed. | 277 | /// The share's room, to tell guests what changed. |
| 402 | room: OnceLock<Sender>, | 278 | room: Mutex<Option<Sender>>, |
| 403 | } | 279 | } |
| 404 | 280 | ||
| 405 | /// A guest as its host serves it: the line to it, its requests waiting for its workers, how | 281 | /// A guest as its host serves it: the line to it, its requests waiting for its workers, how |
| ... | @@ -515,7 +391,7 @@ impl Served { | ... | @@ -515,7 +391,7 @@ impl Served { |
| 515 | .map(|path| self.storage.stamp(path).ok().map(|stamp| digest(&stamp))) | 391 | .map(|path| self.storage.stamp(path).ok().map(|stamp| digest(&stamp))) |
| 516 | .collect(), | 392 | .collect(), |
| 517 | }; | 393 | }; |
| 518 | if let Some(room) = self.room.get() { | 394 | if let Some(room) = self.room.lock().unwrap().as_ref() { |
| 519 | room.send(kind::TOUCHED, &touched, None); | 395 | room.send(kind::TOUCHED, &touched, None); |
| 520 | } | 396 | } |
| 521 | } | 397 | } |
| ... | @@ -538,6 +414,15 @@ impl Served { | ... | @@ -538,6 +414,15 @@ impl Served { |
| 538 | } | 414 | } |
| 539 | }; | 415 | }; |
| 540 | let id = request.id; | 416 | let id = request.id; |
| 417 | if !self.guests.lock().unwrap().contains_key(peer) { | ||
| 418 | return failed( | ||
| 419 | id, | ||
| 420 | refused( | ||
| 421 | io::ErrorKind::PermissionDenied, | ||
| 422 | "The device is no longer connected", | ||
| 423 | ), | ||
| 424 | ); | ||
| 425 | } | ||
| 541 | match self.answer(peer, kind, request) { | 426 | match self.answer(peer, kind, request) { |
| 542 | Ok(reply) => Reply { id, ..reply }, | 427 | Ok(reply) => Reply { id, ..reply }, |
| 543 | Err(error) => failed(id, error), | 428 | Err(error) => failed(id, error), |
| ... | @@ -864,7 +749,7 @@ impl Served { | ... | @@ -864,7 +749,7 @@ impl Served { |
| 864 | }) | 749 | }) |
| 865 | .collect(); | 750 | .collect(); |
| 866 | drop(guests); | 751 | drop(guests); |
| 867 | if let (Some(room), false) = (self.room.get(), holding.is_empty()) { | 752 | if let (Some(room), false) = (self.room.lock().unwrap().as_ref(), holding.is_empty()) { |
| 868 | room.send(kind::DELTA, &delta, Some(&holding)); | 753 | room.send(kind::DELTA, &delta, Some(&holding)); |
| 869 | } | 754 | } |
| 870 | } | 755 | } |
| ... | @@ -1070,6 +955,12 @@ pub struct Guest { | ... | @@ -1070,6 +955,12 @@ pub struct Guest { |
| 1070 | 955 | ||
| 1071 | struct Inner { | 956 | struct Inner { |
| 1072 | share: [u8; 16], | 957 | share: [u8; 16], |
| 958 | me: Hello, | ||
| 959 | reach: Option<Reach>, | ||
| 960 | relay: Option<String>, | ||
| 961 | events: Arc<dyn Fn() + Send + Sync>, | ||
| 962 | presence: Mutex<Option<([u8; 16], Live)>>, | ||
| 963 | here: Mutex<Presence>, | ||
| 1073 | /// The host and the line to it, while connected. | 964 | /// The host and the line to it, while connected. |
| 1074 | host: Mutex<Option<(Arc<Hello>, Line)>>, | 965 | host: Mutex<Option<(Arc<Hello>, Line)>>, |
| 1075 | pending: Mutex<HashMap<u64, mpsc::Sender<Reply>>>, | 966 | pending: Mutex<HashMap<u64, mpsc::Sender<Reply>>>, |
| ... | @@ -1077,7 +968,7 @@ struct Inner { | ... | @@ -1077,7 +968,7 @@ struct Inner { |
| 1077 | /// Where the host's reports of changed files go, while a background watches. | 968 | /// Where the host's reports of changed files go, while a background watches. |
| 1078 | watch: Mutex<Option<Reports>>, | 969 | watch: Mutex<Option<Reports>>, |
| 1079 | /// The host stopped sharing. | 970 | /// The host stopped sharing. |
| 1080 | stopped: AtomicBool, | 971 | ended: Mutex<Option<Ended>>, |
| 1081 | /// Bytes of chunks asked for and not yet given. | 972 | /// Bytes of chunks asked for and not yet given. |
| 1082 | asked: (Mutex<usize>, Condvar), | 973 | asked: (Mutex<usize>, Condvar), |
| 1083 | /// The sections read lately, kept as the host's deltas change them, newest first. | 974 | /// The sections read lately, kept as the host's deltas change them, newest first. |
| ... | @@ -1087,6 +978,11 @@ struct Inner { | ... | @@ -1087,6 +978,11 @@ struct Inner { |
| 1087 | current: Mutex<HashMap<String, Stamp>>, | 978 | current: Mutex<HashMap<String, Stamp>>, |
| 1088 | } | 979 | } |
| 1089 | 980 | ||
| 981 | enum Ended { | ||
| 982 | Stopped, | ||
| 983 | Removed, | ||
| 984 | } | ||
| 985 | |||
| 1090 | impl Guest { | 986 | impl Guest { |
| 1091 | /// Joins share `share` through its `secret` as `me`, where `reach` and `relay` say. | 987 | /// Joins share `share` through its `secret` as `me`, where `reach` and `relay` say. |
| 1092 | /// `events` runs on a network thread whenever the host comes or goes, or the peers change. | 988 | /// `events` runs on a network thread whenever the host comes or goes, or the peers change. |
| ... | @@ -1100,11 +996,17 @@ impl Guest { | ... | @@ -1100,11 +996,17 @@ impl Guest { |
| 1100 | ) -> io::Result<Arc<Self>> { | 996 | ) -> io::Result<Arc<Self>> { |
| 1101 | let inner = Arc::new(Inner { | 997 | let inner = Arc::new(Inner { |
| 1102 | share, | 998 | share, |
| 999 | me: me.clone(), | ||
| 1000 | reach, | ||
| 1001 | relay: relay.map(str::to_owned), | ||
| 1002 | events: Arc::new(events), | ||
| 1003 | presence: Mutex::default(), | ||
| 1004 | here: Mutex::default(), | ||
| 1103 | host: Mutex::default(), | 1005 | host: Mutex::default(), |
| 1104 | pending: Mutex::default(), | 1006 | pending: Mutex::default(), |
| 1105 | next: AtomicU64::new(1), | 1007 | next: AtomicU64::new(1), |
| 1106 | watch: Mutex::default(), | 1008 | watch: Mutex::default(), |
| 1107 | stopped: AtomicBool::new(false), | 1009 | ended: Mutex::default(), |
| 1108 | asked: Default::default(), | 1010 | asked: Default::default(), |
| 1109 | images: Mutex::default(), | 1011 | images: Mutex::default(), |
| 1110 | current: Mutex::default(), | 1012 | current: Mutex::default(), |
| ... | @@ -1112,7 +1014,7 @@ impl Guest { | ... | @@ -1112,7 +1014,7 @@ impl Guest { |
| 1112 | let heard = Arc::clone(&inner); | 1014 | let heard = Arc::clone(&inner); |
| 1113 | let live = Live::start(me, &Room::Notebook(secret), reach, relay, move |event| { | 1015 | let live = Live::start(me, &Room::Notebook(secret), reach, relay, move |event| { |
| 1114 | heard.heard(event); | 1016 | heard.heard(event); |
| 1115 | events(); | 1017 | (heard.events)(); |
| 1116 | })?; | 1018 | })?; |
| 1117 | Ok(Arc::new(Self { live, inner })) | 1019 | Ok(Arc::new(Self { live, inner })) |
| 1118 | } | 1020 | } |
| ... | @@ -1130,16 +1032,25 @@ impl Guest { | ... | @@ -1130,16 +1032,25 @@ impl Guest { |
| 1130 | 1032 | ||
| 1131 | /// Whether the host said it stopped sharing. | 1033 | /// Whether the host said it stopped sharing. |
| 1132 | pub fn stopped(&self) -> bool { | 1034 | pub fn stopped(&self) -> bool { |
| 1133 | self.inner.stopped.load(Ordering::Acquire) | 1035 | self.inner.ended.lock().unwrap().is_some() |
| 1134 | } | 1036 | } |
| 1135 | 1037 | ||
| 1136 | /// Everyone in the share's room: the host and the other guests. | 1038 | /// Everyone in the share's room: the host and the other guests. |
| 1137 | pub fn peers(&self) -> Vec<Peer> { | 1039 | pub fn peers(&self) -> Vec<Peer> { |
| 1138 | self.live.peers() | 1040 | self.inner |
| 1041 | .presence | ||
| 1042 | .lock() | ||
| 1043 | .unwrap() | ||
| 1044 | .as_ref() | ||
| 1045 | .map(|(_, live)| live.peers()) | ||
| 1046 | .unwrap_or_default() | ||
| 1139 | } | 1047 | } |
| 1140 | 1048 | ||
| 1141 | pub fn set_presence(&self, presence: Presence) { | 1049 | pub fn set_presence(&self, presence: Presence) { |
| 1142 | self.live.set_presence(presence); | 1050 | *self.inner.here.lock().unwrap() = presence.clone(); |
| 1051 | if let Some((_, live)) = &*self.inner.presence.lock().unwrap() { | ||
| 1052 | live.set_presence(presence); | ||
| 1053 | } | ||
| 1143 | } | 1054 | } |
| 1144 | 1055 | ||
| 1145 | /// Sends the host's reports of changed files to `reports` while it stays connected. | 1056 | /// Sends the host's reports of changed files to `reports` while it stays connected. |
| ... | @@ -1152,10 +1063,15 @@ impl Guest { | ... | @@ -1152,10 +1063,15 @@ impl Guest { |
| 1152 | } | 1063 | } |
| 1153 | 1064 | ||
| 1154 | fn offline(&self) -> io::Error { | 1065 | fn offline(&self) -> io::Error { |
| 1155 | let message = if self.stopped() { | 1066 | if let Some(version) = self.live.other_version() { |
| 1156 | "The host stopped sharing this notebook" | 1067 | return io::Error::new(io::ErrorKind::NotConnected, wire::Version(version)); |
| 1157 | } else { | 1068 | } |
| 1158 | "The computer sharing this notebook can’t be reached" | 1069 | let message = match &*self.inner.ended.lock().unwrap() { |
| 1070 | Some(Ended::Stopped) => "The host stopped sharing this notebook", | ||
| 1071 | Some(Ended::Removed) => { | ||
| 1072 | "This device was removed. Ask the person sharing for a new link or code." | ||
| 1073 | } | ||
| 1074 | None => "The computer sharing this notebook can’t be reached", | ||
| 1159 | }; | 1075 | }; |
| 1160 | io::Error::new(io::ErrorKind::NotConnected, message) | 1076 | io::Error::new(io::ErrorKind::NotConnected, message) |
| 1161 | } | 1077 | } |
| ... | @@ -1420,14 +1336,23 @@ impl Inner { | ... | @@ -1420,14 +1336,23 @@ impl Inner { |
| 1420 | } | 1336 | } |
| 1421 | } | 1337 | } |
| 1422 | 1338 | ||
| 1423 | fn heard(&self, event: Event) { | 1339 | fn heard(self: &Arc<Self>, event: Event) { |
| 1424 | let serves = |hello: &Hello| hello.serves == Some(self.share); | 1340 | let serves = |hello: &Hello| { |
| 1341 | hello.serves == Some(self.share) | ||
| 1342 | || self | ||
| 1343 | .host | ||
| 1344 | .lock() | ||
| 1345 | .unwrap() | ||
| 1346 | .as_ref() | ||
| 1347 | .is_some_and(|(host, _)| host.peer == hello.peer) | ||
| 1348 | }; | ||
| 1425 | match event { | 1349 | match event { |
| 1426 | Event::Met(hello, line) if serves(hello) => { | 1350 | Event::Met(hello, line) if serves(hello) => { |
| 1427 | *self.host.lock().unwrap() = Some((Arc::clone(hello), line.clone())); | 1351 | *self.host.lock().unwrap() = Some((Arc::clone(hello), line.clone())); |
| 1428 | } | 1352 | } |
| 1429 | Event::Left(hello) if serves(hello) => { | 1353 | Event::Left(hello) if serves(hello) => { |
| 1430 | *self.host.lock().unwrap() = None; | 1354 | *self.host.lock().unwrap() = None; |
| 1355 | drop(self.presence.lock().unwrap().take()); | ||
| 1431 | self.current.lock().unwrap().clear(); | 1356 | self.current.lock().unwrap().clear(); |
| 1432 | // Each request waiting hears its answer was lost. | 1357 | // Each request waiting hears its answer was lost. |
| 1433 | self.pending.lock().unwrap().clear(); | 1358 | self.pending.lock().unwrap().clear(); |
| ... | @@ -1438,6 +1363,11 @@ impl Inner { | ... | @@ -1438,6 +1363,11 @@ impl Inner { |
| 1438 | Event::Frame { | 1363 | Event::Frame { |
| 1439 | from, kind, body, .. | 1364 | from, kind, body, .. |
| 1440 | } => match kind { | 1365 | } => match kind { |
| 1366 | kind::WELCOME if serves(from) => { | ||
| 1367 | if let Ok(welcome) = minicbor::decode::<Welcome>(body) { | ||
| 1368 | self.join_presence(welcome.room); | ||
| 1369 | } | ||
| 1370 | } | ||
| 1441 | kind::REPLY if serves(from) => { | 1371 | kind::REPLY if serves(from) => { |
| 1442 | if let Ok(reply) = minicbor::decode::<Reply>(body) | 1372 | if let Ok(reply) = minicbor::decode::<Reply>(body) |
| 1443 | && let Some(waiting) = self.pending.lock().unwrap().remove(&reply.id) | 1373 | && let Some(waiting) = self.pending.lock().unwrap().remove(&reply.id) |
| ... | @@ -1458,18 +1388,59 @@ impl Inner { | ... | @@ -1458,18 +1388,59 @@ impl Inner { |
| 1458 | } | 1388 | } |
| 1459 | } | 1389 | } |
| 1460 | } | 1390 | } |
| 1461 | kind::BYE | 1391 | kind::BYE if serves(from) => { |
| 1462 | if serves(from) | 1392 | if let Ok(bye) = minicbor::decode::<wire::Bye>(body) { |
| 1463 | && minicbor::decode::<wire::Bye>(body) | 1393 | let ended = match bye.reason.as_str() { |
| 1464 | .is_ok_and(|bye| bye.reason == STOPPED) => | 1394 | STOPPED => Some(Ended::Stopped), |
| 1465 | { | 1395 | "removed" => Some(Ended::Removed), |
| 1466 | self.stopped.store(true, Ordering::Release); | 1396 | _ => None, |
| 1397 | }; | ||
| 1398 | if ended.is_some() { | ||
| 1399 | *self.ended.lock().unwrap() = ended; | ||
| 1400 | } | ||
| 1401 | } | ||
| 1467 | } | 1402 | } |
| 1468 | _ => {} | 1403 | _ => {} |
| 1469 | }, | 1404 | }, |
| 1470 | _ => {} | 1405 | _ => {} |
| 1471 | } | 1406 | } |
| 1472 | } | 1407 | } |
| 1408 | |||
| 1409 | fn join_presence(self: &Arc<Self>, secret: [u8; 16]) { | ||
| 1410 | let mut presence = self.presence.lock().unwrap(); | ||
| 1411 | if presence.as_ref().is_some_and(|(held, _)| *held == secret) { | ||
| 1412 | return; | ||
| 1413 | } | ||
| 1414 | let inner = Arc::downgrade(self); | ||
| 1415 | let live = Live::start( | ||
| 1416 | self.me.clone(), | ||
| 1417 | &Room::Notebook(secret), | ||
| 1418 | self.reach, | ||
| 1419 | self.relay.as_deref(), | ||
| 1420 | move |event| { | ||
| 1421 | if let Some(inner) = inner.upgrade() { | ||
| 1422 | if matches!(event, Event::Frame { .. }) { | ||
| 1423 | inner.heard(event); | ||
| 1424 | } | ||
| 1425 | (inner.events)(); | ||
| 1426 | } | ||
| 1427 | }, | ||
| 1428 | ); | ||
| 1429 | match live { | ||
| 1430 | Ok(live) => { | ||
| 1431 | live.set_presence(self.here.lock().unwrap().clone()); | ||
| 1432 | *presence = Some((secret, live)); | ||
| 1433 | let mut current = self.current.lock().unwrap(); | ||
| 1434 | let paths: Vec<String> = current.keys().cloned().collect(); | ||
| 1435 | current.clear(); | ||
| 1436 | drop(current); | ||
| 1437 | if let Some(reports) = &*self.watch.lock().unwrap() { | ||
| 1438 | reports.touched(&paths); | ||
| 1439 | } | ||
| 1440 | } | ||
| 1441 | Err(error) => eprintln!("Live Share: could not join presence: {error}"), | ||
| 1442 | } | ||
| 1443 | } | ||
| 1473 | } | 1444 | } |
| 1474 | 1445 | ||
| 1475 | /// A section a Live Share host serves, as a guest's replica publishes to it. | 1446 | /// A section a Live Share host serves, as a guest's replica publishes to it. |
crates/notebook/src/live/share/batch.rs+6| ... | @@ -148,6 +148,12 @@ impl Served { | ... | @@ -148,6 +148,12 @@ impl Served { |
| 148 | image: &[u8], | 148 | image: &[u8], |
| 149 | batch: &Waiting, | 149 | batch: &Waiting, |
| 150 | ) -> Result<Option<Transaction>> { | 150 | ) -> Result<Option<Transaction>> { |
| 151 | if !self.guests.lock().unwrap().contains_key(&batch.peer) { | ||
| 152 | return Err(refused( | ||
| 153 | io::ErrorKind::PermissionDenied, | ||
| 154 | "The device is no longer connected", | ||
| 155 | )); | ||
| 156 | } | ||
| 151 | let stamp: Stamp = batch | 157 | let stamp: Stamp = batch |
| 152 | .request | 158 | .request |
| 153 | .stamp | 159 | .stamp |
crates/notebook/src/live/share/membership.rs created+391| ... | @@ -0,0 +1,391 @@ | ||
| 1 | use super::*; | ||
| 2 | |||
| 3 | pub struct Host { | ||
| 4 | members: Arc<Members>, | ||
| 5 | pairing: Mutex<Option<Live>>, | ||
| 6 | } | ||
| 7 | |||
| 8 | #[allow(clippy::type_complexity)] | ||
| 9 | struct Members { | ||
| 10 | me: Hello, | ||
| 11 | notebook: String, | ||
| 12 | reach: Option<Reach>, | ||
| 13 | relay: Option<String>, | ||
| 14 | sharing: Mutex<Sharing>, | ||
| 15 | changes: Mutex<()>, | ||
| 16 | pending: Mutex<BTreeMap<[u8; 16], (Arc<Hello>, Line)>>, | ||
| 17 | access: Mutex<HashMap<[u8; 16], Live>>, | ||
| 18 | room: Mutex<Option<Live>>, | ||
| 19 | served: Arc<Served>, | ||
| 20 | events: Arc<dyn Fn() + Send + Sync>, | ||
| 21 | keep: Box<dyn Fn(&Sharing) -> io::Result<()> + Send + Sync>, | ||
| 22 | } | ||
| 23 | |||
| 24 | impl Host { | ||
| 25 | /// Shares a notebook, keeping every credential change before granting or revoking access. | ||
| 26 | #[allow(clippy::too_many_arguments)] | ||
| 27 | pub fn start( | ||
| 28 | storage: Box<dyn Storage>, | ||
| 29 | me: Hello, | ||
| 30 | sharing: Sharing, | ||
| 31 | notebook: &str, | ||
| 32 | reach: Option<Reach>, | ||
| 33 | relay: Option<&str>, | ||
| 34 | events: impl Fn() + Send + Sync + 'static, | ||
| 35 | keep: impl Fn(&Sharing) -> io::Result<()> + Send + Sync + 'static, | ||
| 36 | ) -> io::Result<Self> { | ||
| 37 | keep(&sharing)?; | ||
| 38 | let members = Arc::new(Members { | ||
| 39 | me, | ||
| 40 | notebook: notebook.to_owned(), | ||
| 41 | reach, | ||
| 42 | relay: relay.map(str::to_owned), | ||
| 43 | sharing: Mutex::new(sharing), | ||
| 44 | changes: Mutex::default(), | ||
| 45 | pending: Mutex::default(), | ||
| 46 | access: Mutex::default(), | ||
| 47 | room: Mutex::default(), | ||
| 48 | served: Arc::new(Served { | ||
| 49 | storage, | ||
| 50 | images: Mutex::default(), | ||
| 51 | snapshots: Mutex::default(), | ||
| 52 | puts: Mutex::default(), | ||
| 53 | guests: Mutex::default(), | ||
| 54 | writers: Mutex::default(), | ||
| 55 | host: Mutex::default(), | ||
| 56 | room: Mutex::default(), | ||
| 57 | }), | ||
| 58 | events: Arc::new(events), | ||
| 59 | keep: Box::new(keep), | ||
| 60 | }); | ||
| 61 | let sharing = members.sharing.lock().unwrap().clone(); | ||
| 62 | members.presence(sharing.secret)?; | ||
| 63 | for device in &sharing.members { | ||
| 64 | members.open(device.secret)?; | ||
| 65 | } | ||
| 66 | let pairing = Mutex::new(Some(members.pair(&sharing)?)); | ||
| 67 | Ok(Self { members, pairing }) | ||
| 68 | } | ||
| 69 | |||
| 70 | pub fn code(&self) -> Option<String> { | ||
| 71 | let mut pairing = self.pairing.lock().unwrap(); | ||
| 72 | let pairing = pairing.as_mut()?; | ||
| 73 | let mut sharing = self.members.sharing.lock().unwrap(); | ||
| 74 | if pairing.burned() { | ||
| 75 | let mut next = sharing.clone(); | ||
| 76 | next.code = super::super::code::secret().ok()?; | ||
| 77 | (self.members.keep)(&next).ok()?; | ||
| 78 | *pairing = self.members.pair(&next).ok()?; | ||
| 79 | *sharing = next; | ||
| 80 | } | ||
| 81 | let code = pairing.code(); | ||
| 82 | if let Some(code) = &code | ||
| 83 | && *code != sharing.code | ||
| 84 | { | ||
| 85 | let mut next = sharing.clone(); | ||
| 86 | next.code = code.clone(); | ||
| 87 | (self.members.keep)(&next).ok()?; | ||
| 88 | *sharing = next; | ||
| 89 | } | ||
| 90 | code | ||
| 91 | } | ||
| 92 | |||
| 93 | pub fn sharing(&self) -> Sharing { | ||
| 94 | self.code(); | ||
| 95 | self.members.sharing.lock().unwrap().clone() | ||
| 96 | } | ||
| 97 | |||
| 98 | pub fn relayed(&self) -> Relayed { | ||
| 99 | self.pairing | ||
| 100 | .lock() | ||
| 101 | .unwrap() | ||
| 102 | .as_ref() | ||
| 103 | .map_or(Relayed::Unknown, Live::relayed) | ||
| 104 | } | ||
| 105 | |||
| 106 | pub fn guests(&self) -> Vec<Peer> { | ||
| 107 | self.members | ||
| 108 | .room | ||
| 109 | .lock() | ||
| 110 | .unwrap() | ||
| 111 | .as_ref() | ||
| 112 | .map(Live::peers) | ||
| 113 | .unwrap_or_default() | ||
| 114 | } | ||
| 115 | |||
| 116 | pub fn devices(&self) -> Vec<(Device, bool)> { | ||
| 117 | let sharing = self.members.sharing.lock().unwrap(); | ||
| 118 | let access = self.members.access.lock().unwrap(); | ||
| 119 | sharing | ||
| 120 | .members | ||
| 121 | .iter() | ||
| 122 | .map(|device| { | ||
| 123 | let connected = access | ||
| 124 | .get(&device.secret) | ||
| 125 | .is_some_and(|live| !live.peers().is_empty()); | ||
| 126 | (device.clone(), connected) | ||
| 127 | }) | ||
| 128 | .collect() | ||
| 129 | } | ||
| 130 | |||
| 131 | pub fn requests(&self) -> Vec<Arc<Hello>> { | ||
| 132 | self.members | ||
| 133 | .pending | ||
| 134 | .lock() | ||
| 135 | .unwrap() | ||
| 136 | .values() | ||
| 137 | .map(|(hello, _)| Arc::clone(hello)) | ||
| 138 | .collect() | ||
| 139 | } | ||
| 140 | |||
| 141 | pub fn approve(&self, approve: bool) -> io::Result<()> { | ||
| 142 | let mut sharing = self.members.sharing.lock().unwrap(); | ||
| 143 | let mut next = sharing.clone(); | ||
| 144 | next.approve = approve; | ||
| 145 | (self.members.keep)(&next)?; | ||
| 146 | *sharing = next; | ||
| 147 | drop(sharing); | ||
| 148 | if !approve { | ||
| 149 | for hello in self.requests() { | ||
| 150 | self.allow(&hello.peer)?; | ||
| 151 | } | ||
| 152 | } | ||
| 153 | (self.members.events)(); | ||
| 154 | Ok(()) | ||
| 155 | } | ||
| 156 | |||
| 157 | pub fn allow(&self, peer: &[u8; 16]) -> io::Result<()> { | ||
| 158 | let pending = self.members.pending.lock().unwrap().remove(peer); | ||
| 159 | if let Some((hello, line)) = pending { | ||
| 160 | if let Err(error) = self.members.grant(&hello, &line) { | ||
| 161 | self.members | ||
| 162 | .pending | ||
| 163 | .lock() | ||
| 164 | .unwrap() | ||
| 165 | .insert(*peer, (hello, line)); | ||
| 166 | return Err(error); | ||
| 167 | } | ||
| 168 | (self.members.events)(); | ||
| 169 | } | ||
| 170 | Ok(()) | ||
| 171 | } | ||
| 172 | |||
| 173 | pub fn decline(&self, peer: &[u8; 16]) { | ||
| 174 | if let Some((_, line)) = self.members.pending.lock().unwrap().remove(peer) { | ||
| 175 | let _ = line.send(kind::APPROVAL, &wire::Approval::Declined); | ||
| 176 | (self.members.events)(); | ||
| 177 | } | ||
| 178 | } | ||
| 179 | |||
| 180 | pub fn remove(&self, secret: &[u8; 16]) -> io::Result<()> { | ||
| 181 | let _change = self.members.changes.lock().unwrap(); | ||
| 182 | let mut sharing = self.members.sharing.lock().unwrap(); | ||
| 183 | if !sharing | ||
| 184 | .members | ||
| 185 | .iter() | ||
| 186 | .any(|device| device.secret == *secret) | ||
| 187 | { | ||
| 188 | return Ok(()); | ||
| 189 | } | ||
| 190 | let mut next = sharing.clone(); | ||
| 191 | next.members.retain(|device| device.secret != *secret); | ||
| 192 | getrandom::fill(&mut next.secret) | ||
| 193 | .map_err(|_| io::Error::other("System random source failed"))?; | ||
| 194 | next.code = super::super::code::secret()?; | ||
| 195 | (self.members.keep)(&next)?; | ||
| 196 | *sharing = next.clone(); | ||
| 197 | drop(sharing); | ||
| 198 | let removed = self.members.access.lock().unwrap().remove(secret); | ||
| 199 | if let Some(removed) = removed { | ||
| 200 | for peer in removed.peers() { | ||
| 201 | self.members.served.forget(&peer.hello.peer); | ||
| 202 | } | ||
| 203 | removed.leave("removed"); | ||
| 204 | } | ||
| 205 | self.members.presence(next.secret)?; | ||
| 206 | *self.pairing.lock().unwrap() = Some(self.members.pair(&next)?); | ||
| 207 | let access = self.members.access.lock().unwrap(); | ||
| 208 | for device in &next.members { | ||
| 209 | if let Some(live) = access.get(&device.secret) { | ||
| 210 | live.sender().send( | ||
| 211 | kind::WELCOME, | ||
| 212 | &self.members.welcome(&next, device.secret), | ||
| 213 | None, | ||
| 214 | ); | ||
| 215 | } | ||
| 216 | } | ||
| 217 | (self.members.events)(); | ||
| 218 | Ok(()) | ||
| 219 | } | ||
| 220 | |||
| 221 | pub fn set_presence(&self, presence: Presence) { | ||
| 222 | if let Some(room) = &*self.members.room.lock().unwrap() { | ||
| 223 | room.set_presence(presence); | ||
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 227 | pub fn on_changed(&self, listener: crate::session::Listener) { | ||
| 228 | *self.members.served.host.lock().unwrap() = Some(listener); | ||
| 229 | } | ||
| 230 | |||
| 231 | pub fn touched(&self, paths: &[String]) { | ||
| 232 | for path in paths { | ||
| 233 | self.members.served.changed_here(path); | ||
| 234 | } | ||
| 235 | self.members.served.tell(paths); | ||
| 236 | } | ||
| 237 | |||
| 238 | pub fn stop(&self) { | ||
| 239 | let _change = self.members.changes.lock().unwrap(); | ||
| 240 | drop(self.pairing.lock().unwrap().take()); | ||
| 241 | self.members.pending.lock().unwrap().clear(); | ||
| 242 | let access = std::mem::take(&mut *self.members.access.lock().unwrap()); | ||
| 243 | for (_, live) in access { | ||
| 244 | live.leave(STOPPED); | ||
| 245 | } | ||
| 246 | drop(self.members.room.lock().unwrap().take()); | ||
| 247 | } | ||
| 248 | } | ||
| 249 | |||
| 250 | impl Members { | ||
| 251 | fn welcome(&self, sharing: &Sharing, secret: [u8; 16]) -> Welcome { | ||
| 252 | Welcome { | ||
| 253 | share: sharing.share, | ||
| 254 | secret, | ||
| 255 | room: sharing.secret, | ||
| 256 | notebook: self.notebook.clone(), | ||
| 257 | host: self.me.name.clone(), | ||
| 258 | } | ||
| 259 | } | ||
| 260 | |||
| 261 | fn presence(self: &Arc<Self>, secret: [u8; 16]) -> io::Result<()> { | ||
| 262 | let told = Arc::clone(&self.events); | ||
| 263 | let live = Live::start( | ||
| 264 | self.me.clone(), | ||
| 265 | &Room::Notebook(secret), | ||
| 266 | self.reach, | ||
| 267 | self.relay.as_deref(), | ||
| 268 | move |_| told(), | ||
| 269 | )?; | ||
| 270 | *self.served.room.lock().unwrap() = Some(live.sender()); | ||
| 271 | *self.room.lock().unwrap() = Some(live); | ||
| 272 | Ok(()) | ||
| 273 | } | ||
| 274 | |||
| 275 | fn open(self: &Arc<Self>, secret: [u8; 16]) -> io::Result<()> { | ||
| 276 | let members = Arc::downgrade(self); | ||
| 277 | let live = Live::start( | ||
| 278 | Hello { | ||
| 279 | serves: Some(self.sharing.lock().unwrap().share), | ||
| 280 | ..self.me.clone() | ||
| 281 | }, | ||
| 282 | &Room::Notebook(secret), | ||
| 283 | self.reach, | ||
| 284 | self.relay.as_deref(), | ||
| 285 | move |event| { | ||
| 286 | let Some(members) = members.upgrade() else { | ||
| 287 | return; | ||
| 288 | }; | ||
| 289 | match event { | ||
| 290 | Event::Met(hello, line) => { | ||
| 291 | let sharing = members.sharing.lock().unwrap(); | ||
| 292 | if !sharing.members.iter().any(|device| device.secret == secret) { | ||
| 293 | line.hang_up("removed"); | ||
| 294 | return; | ||
| 295 | } | ||
| 296 | members.served.admit(hello.peer, line); | ||
| 297 | let _ = line.send(kind::WELCOME, &members.welcome(&sharing, secret)); | ||
| 298 | } | ||
| 299 | Event::Left(hello) => members.served.forget(&hello.peer), | ||
| 300 | Event::Frame { from, kind, body } | ||
| 301 | if wire::KNOWN.contains(&kind) && kind > 256 && kind != kind::REPLY => | ||
| 302 | { | ||
| 303 | members.served.queue(&from.peer, kind, body) | ||
| 304 | } | ||
| 305 | Event::Changed => (members.events)(), | ||
| 306 | _ => {} | ||
| 307 | } | ||
| 308 | }, | ||
| 309 | )?; | ||
| 310 | self.access.lock().unwrap().insert(secret, live); | ||
| 311 | Ok(()) | ||
| 312 | } | ||
| 313 | |||
| 314 | fn grant(self: &Arc<Self>, hello: &Hello, line: &Line) -> io::Result<()> { | ||
| 315 | let _change = self.changes.lock().unwrap(); | ||
| 316 | if self.room.lock().unwrap().is_none() { | ||
| 317 | return Err(io::ErrorKind::NotConnected.into()); | ||
| 318 | } | ||
| 319 | let mut secret = [0; 16]; | ||
| 320 | getrandom::fill(&mut secret) | ||
| 321 | .map_err(|_| io::Error::other("System random source failed"))?; | ||
| 322 | if self.sharing.lock().unwrap().members.len() >= 64 { | ||
| 323 | return Err(io::ErrorKind::ResourceBusy.into()); | ||
| 324 | } | ||
| 325 | self.open(secret)?; | ||
| 326 | let kept = (|| -> io::Result<Welcome> { | ||
| 327 | let mut sharing = self.sharing.lock().unwrap(); | ||
| 328 | let mut next = sharing.clone(); | ||
| 329 | next.members.push(Device { | ||
| 330 | secret, | ||
| 331 | name: hello.name.clone(), | ||
| 332 | device: hello.device.clone(), | ||
| 333 | }); | ||
| 334 | (self.keep)(&next)?; | ||
| 335 | *sharing = next; | ||
| 336 | Ok(self.welcome(&sharing, secret)) | ||
| 337 | })(); | ||
| 338 | let welcome = match kept { | ||
| 339 | Ok(welcome) => welcome, | ||
| 340 | Err(error) => { | ||
| 341 | drop(self.access.lock().unwrap().remove(&secret)); | ||
| 342 | return Err(error); | ||
| 343 | } | ||
| 344 | }; | ||
| 345 | line.send(kind::WELCOME, &welcome)?; | ||
| 346 | (self.events)(); | ||
| 347 | Ok(()) | ||
| 348 | } | ||
| 349 | |||
| 350 | fn pair(self: &Arc<Self>, sharing: &Sharing) -> io::Result<Live> { | ||
| 351 | let members = Arc::downgrade(self); | ||
| 352 | Live::start( | ||
| 353 | Hello { | ||
| 354 | serves: None, | ||
| 355 | ..self.me.clone() | ||
| 356 | }, | ||
| 357 | &Room::share(&sharing.code, &sharing.password), | ||
| 358 | self.reach, | ||
| 359 | self.relay.as_deref(), | ||
| 360 | move |event| { | ||
| 361 | let Some(members) = members.upgrade() else { | ||
| 362 | return; | ||
| 363 | }; | ||
| 364 | match event { | ||
| 365 | Event::Met(hello, line) => { | ||
| 366 | if members.sharing.lock().unwrap().approve { | ||
| 367 | let mut pending = members.pending.lock().unwrap(); | ||
| 368 | if pending.len() < 32 { | ||
| 369 | pending.insert(hello.peer, (Arc::clone(hello), line.clone())); | ||
| 370 | let _ = line.send(kind::APPROVAL, &wire::Approval::Pending); | ||
| 371 | } else { | ||
| 372 | let _ = line.send(kind::APPROVAL, &wire::Approval::Failed); | ||
| 373 | } | ||
| 374 | drop(pending); | ||
| 375 | (members.events)(); | ||
| 376 | } else if let Err(error) = members.grant(hello, line) { | ||
| 377 | eprintln!("Live Share: could not admit a device: {error}"); | ||
| 378 | let _ = line.send(kind::APPROVAL, &wire::Approval::Failed); | ||
| 379 | } | ||
| 380 | } | ||
| 381 | Event::Left(hello) => { | ||
| 382 | members.pending.lock().unwrap().remove(&hello.peer); | ||
| 383 | (members.events)(); | ||
| 384 | } | ||
| 385 | Event::Changed => (members.events)(), | ||
| 386 | _ => {} | ||
| 387 | } | ||
| 388 | }, | ||
| 389 | ) | ||
| 390 | } | ||
| 391 | } | ||
crates/notebook/src/live/wire.rs+22-6| ... | @@ -1,7 +1,6 @@ | ... | @@ -1,7 +1,6 @@ |
| 1 | //! What peers say to each other: an opening in the clear that meets through the secret both | 1 | //! What peers say to each other: an opening in the clear that meets through the secret both |
| 2 | //! hold (SPAKE2), then frames sealed under the keys it agreed, each a message kind and a CBOR | 2 | //! hold (SPAKE2), then frames sealed under the keys it agreed, each a message kind and a CBOR |
| 3 | //! map. A later version adds kinds and fields; a reader skips the kinds and fields it doesn't | 3 | //! map. Readers skip unknown kinds and fields within the same opening version. |
| 4 | //! know, so every version speaks to every other. | ||
| 5 | 4 | ||
| 6 | use aes_gcm::{Aes256Gcm, KeyInit, aead::Aead}; | 5 | use aes_gcm::{Aes256Gcm, KeyInit, aead::Aead}; |
| 7 | use hmac::{Hmac, Mac}; | 6 | use hmac::{Hmac, Mac}; |
| ... | @@ -10,8 +9,8 @@ use sha2::Sha256; | ... | @@ -10,8 +9,8 @@ use sha2::Sha256; |
| 10 | use spake2::{Ed25519Group, Identity, Password, Spake2}; | 9 | use spake2::{Ed25519Group, Identity, Password, Spake2}; |
| 11 | use std::io::{self, Read, Write}; | 10 | use std::io::{self, Read, Write}; |
| 12 | 11 | ||
| 13 | /// The opening's version. Frames after it never change shape; they grow by kinds and fields. | 12 | /// The opening's version; peers must agree on its authentication and access rules. |
| 14 | pub const VERSION: u16 = 2; | 13 | pub const VERSION: u16 = 3; |
| 15 | 14 | ||
| 16 | /// The version of the opening a peer of another version sent, as `open` fails with it. | 15 | /// The version of the opening a peer of another version sent, as `open` fails with it. |
| 17 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | 16 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| ... | @@ -45,6 +44,7 @@ pub mod kind { | ... | @@ -45,6 +44,7 @@ pub mod kind { |
| 45 | /// A section's bytes as a commit changed them: `Delta`. | 44 | /// A section's bytes as a commit changed them: `Delta`. |
| 46 | pub const DELTA: u16 = 19; | 45 | pub const DELTA: u16 = 19; |
| 47 | pub const WELCOME: u16 = 32; | 46 | pub const WELCOME: u16 = 32; |
| 47 | pub const APPROVAL: u16 = 33; | ||
| 48 | /// Storage requests to a host, each a `Request` answered by a `Reply`. | 48 | /// Storage requests to a host, each a `Request` answered by a `Reply`. |
| 49 | pub const LIST: u16 = 257; | 49 | pub const LIST: u16 = 257; |
| 50 | pub const STAMP: u16 = 258; | 50 | pub const STAMP: u16 = 258; |
| ... | @@ -79,6 +79,7 @@ pub const KNOWN: &[u16] = &[ | ... | @@ -79,6 +79,7 @@ pub const KNOWN: &[u16] = &[ |
| 79 | kind::TOUCHED, | 79 | kind::TOUCHED, |
| 80 | kind::DELTA, | 80 | kind::DELTA, |
| 81 | kind::WELCOME, | 81 | kind::WELCOME, |
| 82 | kind::APPROVAL, | ||
| 82 | kind::LIST, | 83 | kind::LIST, |
| 83 | kind::STAMP, | 84 | kind::STAMP, |
| 84 | kind::READ, | 85 | kind::READ, |
| ... | @@ -122,6 +123,8 @@ pub struct Hello { | ... | @@ -122,6 +123,8 @@ pub struct Hello { |
| 122 | pub serves: Option<[u8; 16]>, | 123 | pub serves: Option<[u8; 16]>, |
| 123 | #[n(6)] | 124 | #[n(6)] |
| 124 | pub ops: Option<u16>, | 125 | pub ops: Option<u16>, |
| 126 | #[n(7)] | ||
| 127 | pub device: Option<String>, | ||
| 125 | } | 128 | } |
| 126 | 129 | ||
| 127 | impl Hello { | 130 | impl Hello { |
| ... | @@ -137,6 +140,7 @@ impl Hello { | ... | @@ -137,6 +140,7 @@ impl Hello { |
| 137 | kinds: KNOWN.to_vec(), | 140 | kinds: KNOWN.to_vec(), |
| 138 | serves: None, | 141 | serves: None, |
| 139 | ops: Some(1), | 142 | ops: Some(1), |
| 143 | device: None, | ||
| 140 | }) | 144 | }) |
| 141 | } | 145 | } |
| 142 | } | 146 | } |
| ... | @@ -211,8 +215,7 @@ pub struct Bye { | ... | @@ -211,8 +215,7 @@ pub struct Bye { |
| 211 | pub reason: String, | 215 | pub reason: String, |
| 212 | } | 216 | } |
| 213 | 217 | ||
| 214 | /// What the host of a share gives a peer that knew its code: the share's room, and names to | 218 | /// A device's access credential, the current presence room, and the notebook's names. |
| 215 | /// show it by. | ||
| 216 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] | 219 | #[derive(Clone, Debug, PartialEq, Encode, Decode)] |
| 217 | #[cbor(map)] | 220 | #[cbor(map)] |
| 218 | pub struct Welcome { | 221 | pub struct Welcome { |
| ... | @@ -225,6 +228,19 @@ pub struct Welcome { | ... | @@ -225,6 +228,19 @@ pub struct Welcome { |
| 225 | /// The host's name for itself, as `Hello::name`. | 228 | /// The host's name for itself, as `Hello::name`. |
| 226 | #[n(3)] | 229 | #[n(3)] |
| 227 | pub host: String, | 230 | pub host: String, |
| 231 | #[cbor(n(4), with = "minicbor::bytes")] | ||
| 232 | pub room: [u8; 16], | ||
| 233 | } | ||
| 234 | |||
| 235 | #[derive(Clone, Debug, Encode, Decode)] | ||
| 236 | #[cbor(index_only)] | ||
| 237 | pub enum Approval { | ||
| 238 | #[n(0)] | ||
| 239 | Pending, | ||
| 240 | #[n(1)] | ||
| 241 | Declined, | ||
| 242 | #[n(2)] | ||
| 243 | Failed, | ||
| 228 | } | 244 | } |
| 229 | 245 | ||
| 230 | /// Paths a host's files changed at, by catalog path; `""` is the notebook's folder. | 246 | /// Paths a host's files changed at, by catalog path; `""` is the notebook's folder. |
crates/notebook/tests/live_membership.rs created+190| ... | @@ -0,0 +1,190 @@ | ||
| 1 | #![cfg(feature = "live")] | ||
| 2 | |||
| 3 | #[path = "support/live.rs"] | ||
| 4 | mod live; | ||
| 5 | use live::*; | ||
| 6 | use notebook::live::share::{self, Guest, Host, Sharing}; | ||
| 7 | use notebook::session::Notebook; | ||
| 8 | use std::{ | ||
| 9 | sync::{ | ||
| 10 | Arc, Mutex, | ||
| 11 | atomic::{AtomicBool, Ordering}, | ||
| 12 | mpsc, | ||
| 13 | }, | ||
| 14 | time::Duration, | ||
| 15 | }; | ||
| 16 | |||
| 17 | #[test] | ||
| 18 | fn approval_keeps_credentials_before_welcome_and_a_decline_grants_nothing() { | ||
| 19 | let directory = tempfile::tempdir().unwrap(); | ||
| 20 | let folder = notebook(directory.path()); | ||
| 21 | let url = relay(Default::default()); | ||
| 22 | let saved = Arc::new(Mutex::new(None)); | ||
| 23 | let fail = Arc::new(AtomicBool::new(false)); | ||
| 24 | let (kept, failing) = (Arc::clone(&saved), Arc::clone(&fail)); | ||
| 25 | let mut sharing = Sharing::new("").unwrap(); | ||
| 26 | sharing.approve = true; | ||
| 27 | let host = Host::start( | ||
| 28 | Notebook::open(&folder, directory.path().join("host")) | ||
| 29 | .unwrap() | ||
| 30 | .into_storage(), | ||
| 31 | hello("Ada"), | ||
| 32 | sharing, | ||
| 33 | "Garden", | ||
| 34 | None, | ||
| 35 | Some(&url), | ||
| 36 | || {}, | ||
| 37 | move |sharing| { | ||
| 38 | if failing.load(Ordering::Acquire) { | ||
| 39 | return Err(std::io::ErrorKind::PermissionDenied.into()); | ||
| 40 | } | ||
| 41 | *kept.lock().unwrap() = Some(serde_json::to_vec(sharing).unwrap()); | ||
| 42 | Ok(()) | ||
| 43 | }, | ||
| 44 | ) | ||
| 45 | .unwrap(); | ||
| 46 | let code = self::code(&host); | ||
| 47 | let (reply, result) = mpsc::channel(); | ||
| 48 | let joining = url.clone(); | ||
| 49 | std::thread::spawn(move || { | ||
| 50 | let _ = reply.send(share::join(hello("Grace"), &code, "", None, Some(&joining))); | ||
| 51 | }); | ||
| 52 | until("the request never arrived", || host.requests().len() == 1); | ||
| 53 | assert!(result.try_recv().is_err()); | ||
| 54 | assert!(host.devices().is_empty()); | ||
| 55 | let peer = host.requests()[0].peer; | ||
| 56 | fail.store(true, Ordering::Release); | ||
| 57 | assert!(host.allow(&peer).is_err()); | ||
| 58 | assert!(host.devices().is_empty()); | ||
| 59 | assert_eq!(host.requests().len(), 1); | ||
| 60 | fail.store(false, Ordering::Release); | ||
| 61 | host.allow(&peer).unwrap(); | ||
| 62 | let welcome = result | ||
| 63 | .recv_timeout(Duration::from_secs(5)) | ||
| 64 | .unwrap() | ||
| 65 | .unwrap(); | ||
| 66 | let persisted: Sharing = | ||
| 67 | serde_json::from_slice(saved.lock().unwrap().as_ref().unwrap()).unwrap(); | ||
| 68 | assert_eq!(persisted.members[0].secret, welcome.secret); | ||
| 69 | assert_ne!(welcome.secret, welcome.room); | ||
| 70 | fail.store(true, Ordering::Release); | ||
| 71 | assert!(host.remove(&welcome.secret).is_err()); | ||
| 72 | assert_eq!(host.sharing(), persisted); | ||
| 73 | fail.store(false, Ordering::Release); | ||
| 74 | |||
| 75 | let code = self::code(&host); | ||
| 76 | let (reply, result) = mpsc::channel(); | ||
| 77 | std::thread::spawn(move || { | ||
| 78 | let _ = reply.send(share::join(hello("Alan"), &code, "", None, Some(&url))); | ||
| 79 | }); | ||
| 80 | until("the second request never arrived", || { | ||
| 81 | host.requests().len() == 1 | ||
| 82 | }); | ||
| 83 | host.decline(&host.requests()[0].peer); | ||
| 84 | assert_eq!( | ||
| 85 | result.recv_timeout(Duration::from_secs(5)).unwrap(), | ||
| 86 | Err(share::Refusal::Declined) | ||
| 87 | ); | ||
| 88 | assert_eq!(host.devices().len(), 1); | ||
| 89 | } | ||
| 90 | |||
| 91 | #[test] | ||
| 92 | fn removal_retires_one_credential_and_other_devices_reconnect_after_a_restart() { | ||
| 93 | let directory = tempfile::tempdir().unwrap(); | ||
| 94 | let folder = notebook(directory.path()); | ||
| 95 | let url = relay(Default::default()); | ||
| 96 | let sharing = Sharing::new("").unwrap(); | ||
| 97 | let host_cache = directory.path().join("host"); | ||
| 98 | let host = host(&folder, &host_cache, &sharing, &url); | ||
| 99 | let original_code = code(&host); | ||
| 100 | let (alice, _) = guest( | ||
| 101 | "Alice", | ||
| 102 | &original_code, | ||
| 103 | &url, | ||
| 104 | &directory.path().join("alice"), | ||
| 105 | ); | ||
| 106 | let (bob, _) = guest("Bob", &original_code, &url, &directory.path().join("bob")); | ||
| 107 | let before = host.sharing(); | ||
| 108 | let alice_key = before | ||
| 109 | .members | ||
| 110 | .iter() | ||
| 111 | .find(|device| device.name == "Alice") | ||
| 112 | .unwrap() | ||
| 113 | .secret; | ||
| 114 | let bob_key = before | ||
| 115 | .members | ||
| 116 | .iter() | ||
| 117 | .find(|device| device.name == "Bob") | ||
| 118 | .unwrap() | ||
| 119 | .secret; | ||
| 120 | host.remove(&bob_key).unwrap(); | ||
| 121 | until("Bob was not removed", || { | ||
| 122 | bob.stopped() && bob.host().is_none() | ||
| 123 | }); | ||
| 124 | until("presence did not move to the new room", || { | ||
| 125 | host.guests().len() == 1 | ||
| 126 | }); | ||
| 127 | let current = host.sharing(); | ||
| 128 | assert_ne!(before.secret, current.secret); | ||
| 129 | assert_eq!(current.members.len(), 1); | ||
| 130 | assert_ne!(original_code, code(&host)); | ||
| 131 | assert!(alice.host().is_some()); | ||
| 132 | assert!(!alice.stopped()); | ||
| 133 | let live = Notebook::open_hosted(Arc::clone(&alice), directory.path().join("alice")).unwrap(); | ||
| 134 | assert_eq!(live.catalog().sections.len(), 2); | ||
| 135 | let restarted: Sharing = | ||
| 136 | serde_json::from_slice(&serde_json::to_vec(&current).unwrap()).unwrap(); | ||
| 137 | drop(host); | ||
| 138 | until("Alice's connection did not close", || { | ||
| 139 | alice.host().is_none() | ||
| 140 | }); | ||
| 141 | let host = self::host(&folder, &host_cache, &restarted, &url); | ||
| 142 | until("Alice did not reconnect", || alice.host().is_some()); | ||
| 143 | assert_eq!(host.devices()[0].0.secret, alice_key); | ||
| 144 | let forged = Guest::start( | ||
| 145 | hello("Alice"), | ||
| 146 | current.share, | ||
| 147 | bob_key, | ||
| 148 | None, | ||
| 149 | Some(&url), | ||
| 150 | || {}, | ||
| 151 | ) | ||
| 152 | .unwrap(); | ||
| 153 | std::thread::sleep(Duration::from_millis(500)); | ||
| 154 | assert!(forged.host().is_none()); | ||
| 155 | assert!(Notebook::open_hosted(forged, directory.path().join("forged")).is_err()); | ||
| 156 | } | ||
| 157 | |||
| 158 | #[test] | ||
| 159 | fn cancelling_a_join_retires_its_pending_request() { | ||
| 160 | let directory = tempfile::tempdir().unwrap(); | ||
| 161 | let folder = notebook(directory.path()); | ||
| 162 | let url = relay(Default::default()); | ||
| 163 | let mut sharing = Sharing::new("").unwrap(); | ||
| 164 | sharing.approve = true; | ||
| 165 | let host = host(&folder, &directory.path().join("host"), &sharing, &url); | ||
| 166 | let code = code(&host); | ||
| 167 | let alive = Arc::new(AtomicBool::new(true)); | ||
| 168 | let continuing = Arc::clone(&alive); | ||
| 169 | let (reply, result) = mpsc::channel(); | ||
| 170 | std::thread::spawn(move || { | ||
| 171 | let _ = reply.send(share::join_while( | ||
| 172 | hello("Grace"), | ||
| 173 | &code, | ||
| 174 | "", | ||
| 175 | None, | ||
| 176 | Some(&url), | ||
| 177 | |_| continuing.load(Ordering::Acquire), | ||
| 178 | )); | ||
| 179 | }); | ||
| 180 | until("the request never arrived", || !host.requests().is_empty()); | ||
| 181 | alive.store(false, Ordering::Release); | ||
| 182 | assert_eq!( | ||
| 183 | result.recv_timeout(Duration::from_secs(5)).unwrap(), | ||
| 184 | Err(share::Refusal::Cancelled) | ||
| 185 | ); | ||
| 186 | until("the cancelled request stayed", || { | ||
| 187 | host.requests().is_empty() | ||
| 188 | }); | ||
| 189 | assert!(host.devices().is_empty()); | ||
| 190 | } | ||
crates/notebook/tests/live_share.rs+2| ... | @@ -70,6 +70,7 @@ fn a_guest_queues_while_the_host_is_away() { | ... | @@ -70,6 +70,7 @@ fn a_guest_queues_while_the_host_is_away() { |
| 70 | let image = std::fs::read(&file).unwrap(); | 70 | let image = std::fs::read(&file).unwrap(); |
| 71 | 71 | ||
| 72 | // Ada's computer goes to sleep. | 72 | // Ada's computer goes to sleep. |
| 73 | let sharing = host.sharing(); | ||
| 73 | drop(host); | 74 | drop(host); |
| 74 | until("the host never left", || guest.host().is_none()); | 75 | until("the host never left", || guest.host().is_none()); |
| 75 | let id = replace(&section, &image, 0..8, "Offline"); | 76 | let id = replace(&section, &image, 0..8, "Offline"); |
| ... | @@ -110,6 +111,7 @@ fn two_guests_on_one_page_conflict_as_on_a_share() { | ... | @@ -110,6 +111,7 @@ fn two_guests_on_one_page_conflict_as_on_a_share() { |
| 110 | let file = folder.join("Garden.one"); | 111 | let file = folder.join("Garden.one"); |
| 111 | let image = std::fs::read(&file).unwrap(); | 112 | let image = std::fs::read(&file).unwrap(); |
| 112 | 113 | ||
| 114 | let sharing = host.sharing(); | ||
| 113 | drop(host); | 115 | drop(host); |
| 114 | until("the host never left", || { | 116 | until("the host never left", || { |
| 115 | grace.host().is_none() && alan.host().is_none() | 117 | grace.host().is_none() && alan.host().is_none() |
crates/notebook/tests/support/live.rs+1| ... | @@ -83,6 +83,7 @@ pub fn host(folder: &Path, cache: &Path, sharing: &Sharing, url: &str) -> Host { | ... | @@ -83,6 +83,7 @@ pub fn host(folder: &Path, cache: &Path, sharing: &Sharing, url: &str) -> Host { |
| 83 | None, | 83 | None, |
| 84 | Some(url), | 84 | Some(url), |
| 85 | || {}, | 85 | || {}, |
| 86 | |_| Ok(()), | ||
| 86 | ) | 87 | ) |
| 87 | .unwrap() | 88 | .unwrap() |
| 88 | } | 89 | } |
crates/relay/src/main.rs+2-2| ... | @@ -12,14 +12,14 @@ underscores: SNOWBOUND_RELAY_LISTEN=127.0.0.1:23592. Flags win. | ... | @@ -12,14 +12,14 @@ underscores: SNOWBOUND_RELAY_LISTEN=127.0.0.1:23592. Flags win. |
| 12 | --listen ADDRESS where to listen (127.0.0.1:23592) | 12 | --listen ADDRESS where to listen (127.0.0.1:23592) |
| 13 | --trust-forwarded true|false count peers by X-Forwarded-For, behind a proxy (false) | 13 | --trust-forwarded true|false count peers by X-Forwarded-For, behind a proxy (false) |
| 14 | --max-connections N (256) | 14 | --max-connections N (256) |
| 15 | --max-connections-per-address N per IPv4 address or IPv6 /64 (16) | 15 | --max-connections-per-address N per IPv4 address or IPv6 /64 (128) |
| 16 | --max-rooms N (128) | 16 | --max-rooms N (128) |
| 17 | --max-room-peers N (64) | 17 | --max-room-peers N (64) |
| 18 | --max-message BYTES (262144) | 18 | --max-message BYTES (262144) |
| 19 | --queue BYTES waiting to go to one peer before it is dropped (1048576) | 19 | --queue BYTES waiting to go to one peer before it is dropped (1048576) |
| 20 | --idle SECONDS silence before a connection is closed (600) | 20 | --idle SECONDS silence before a connection is closed (600) |
| 21 | --room-bytes-per-second BYTES (4194304) | 21 | --room-bytes-per-second BYTES (4194304) |
| 22 | --joins-per-minute N per address (30) | 22 | --joins-per-minute N per address (240) |
| 23 | --room-joins-per-minute N (120) | 23 | --room-joins-per-minute N (120) |
| 24 | --failures-per-minute N wrong codes per address before a lockout (10) | 24 | --failures-per-minute N wrong codes per address before a lockout (10) |
| 25 | --burn-after N wrong codes before a code admits no one new (5) | 25 | --burn-after N wrong codes before a code admits no one new (5) |
crates/relay/src/server.rs+2-2| ... | @@ -48,14 +48,14 @@ impl Default for Config { | ... | @@ -48,14 +48,14 @@ impl Default for Config { |
| 48 | Self { | 48 | Self { |
| 49 | trust_forwarded: false, | 49 | trust_forwarded: false, |
| 50 | max_connections: 256, | 50 | max_connections: 256, |
| 51 | max_connections_per_address: 16, | 51 | max_connections_per_address: 128, |
| 52 | max_rooms: 128, | 52 | max_rooms: 128, |
| 53 | max_room_peers: 64, | 53 | max_room_peers: 64, |
| 54 | max_message: 256 << 10, | 54 | max_message: 256 << 10, |
| 55 | queue: 1 << 20, | 55 | queue: 1 << 20, |
| 56 | idle: Duration::from_secs(600), | 56 | idle: Duration::from_secs(600), |
| 57 | room_bytes_per_second: 4 << 20, | 57 | room_bytes_per_second: 4 << 20, |
| 58 | joins_per_minute: 30, | 58 | joins_per_minute: 240, |
| 59 | room_joins_per_minute: 120, | 59 | room_joins_per_minute: 120, |
| 60 | failures_per_minute: 10, | 60 | failures_per_minute: 10, |
| 61 | burn_after: 5, | 61 | burn_after: 5, |
crates/snowbound/src/live.rs+64-27| ... | @@ -90,7 +90,17 @@ fn hello() -> io::Result<Hello> { | ... | @@ -90,7 +90,17 @@ fn hello() -> io::Result<Hello> { |
| 90 | let picture = (picture && name == platform::user_name()) | 90 | let picture = (picture && name == platform::user_name()) |
| 91 | .then(|| account_picture().clone()) | 91 | .then(|| account_picture().clone()) |
| 92 | .flatten(); | 92 | .flatten(); |
| 93 | Hello::new(name, picture) | 93 | let mut hello = Hello::new(name, picture)?; |
| 94 | hello.device = std::env::var("COMPUTERNAME").ok().or_else(|| { | ||
| 95 | std::process::Command::new("hostname") | ||
| 96 | .output() | ||
| 97 | .ok() | ||
| 98 | .filter(|output| output.status.success()) | ||
| 99 | .and_then(|output| String::from_utf8(output.stdout).ok()) | ||
| 100 | .map(|name| name.trim().to_owned()) | ||
| 101 | .filter(|name| !name.is_empty()) | ||
| 102 | }); | ||
| 103 | Ok(hello) | ||
| 94 | } | 104 | } |
| 95 | 105 | ||
| 96 | /// The relay peers off this network meet through: `SNOWBOUND_LIVE_RELAY` (`off` for none), | 106 | /// The relay peers off this network meet through: `SNOWBOUND_LIVE_RELAY` (`off` for none), |
| ... | @@ -141,11 +151,13 @@ fn keep(file: &Path, value: &impl serde::Serialize) -> io::Result<()> { | ... | @@ -141,11 +151,13 @@ fn keep(file: &Path, value: &impl serde::Serialize) -> io::Result<()> { |
| 141 | options.write(true).create(true).truncate(true); | 151 | options.write(true).create(true).truncate(true); |
| 142 | #[cfg(unix)] | 152 | #[cfg(unix)] |
| 143 | std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600); | 153 | std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600); |
| 144 | io::Write::write_all( | 154 | let mut output = options.open(&partial)?; |
| 145 | &mut options.open(&partial)?, | 155 | io::Write::write_all(&mut output, &serde_json::to_vec_pretty(value)?)?; |
| 146 | &serde_json::to_vec_pretty(value)?, | 156 | output.sync_all()?; |
| 147 | )?; | 157 | notebook::fs::rename(partial, file)?; |
| 148 | notebook::fs::rename(partial, file) | 158 | #[cfg(unix)] |
| 159 | std::fs::File::open(folder)?.sync_all()?; | ||
| 160 | Ok(()) | ||
| 149 | } | 161 | } |
| 150 | 162 | ||
| 151 | /// A notebook another computer shares, as this one joined it. | 163 | /// A notebook another computer shares, as this one joined it. |
| ... | @@ -157,6 +169,8 @@ struct Share { | ... | @@ -157,6 +169,8 @@ struct Share { |
| 157 | host: String, | 169 | host: String, |
| 158 | /// It has been listed here, so it opens without waiting for the host. | 170 | /// It has been listed here, so it opens without waiting for the host. |
| 159 | listed: bool, | 171 | listed: bool, |
| 172 | #[serde(default)] | ||
| 173 | protocol: u16, | ||
| 160 | } | 174 | } |
| 161 | 175 | ||
| 162 | const JOINED: &str = "joined.json"; | 176 | const JOINED: &str = "joined.json"; |
| ... | @@ -186,6 +200,11 @@ impl Joined { | ... | @@ -186,6 +200,11 @@ impl Joined { |
| 186 | )); | 200 | )); |
| 187 | }; | 201 | }; |
| 188 | let refused = |error: &dyn std::fmt::Display| (share.notebook.clone(), error.to_string()); | 202 | let refused = |error: &dyn std::fmt::Display| (share.notebook.clone(), error.to_string()); |
| 203 | if share.protocol != live::wire::VERSION { | ||
| 204 | return Err(refused( | ||
| 205 | &"Open this notebook again with the person sharing’s current link or code.", | ||
| 206 | )); | ||
| 207 | } | ||
| 189 | let guest = hello() | 208 | let guest = hello() |
| 190 | .and_then(|me| { | 209 | .and_then(|me| { |
| 191 | Guest::start( | 210 | Guest::start( |
| ... | @@ -241,9 +260,10 @@ impl Joined { | ... | @@ -241,9 +260,10 @@ impl Joined { |
| 241 | pub(crate) fn join( | 260 | pub(crate) fn join( |
| 242 | code: &str, | 261 | code: &str, |
| 243 | password: &str, | 262 | password: &str, |
| 263 | waiting: impl Fn(bool) -> bool, | ||
| 244 | ) -> Result<live::wire::Welcome, live::share::Refusal> { | 264 | ) -> Result<live::wire::Welcome, live::share::Refusal> { |
| 245 | let me = hello().map_err(|_| live::share::Refusal::Unreachable(live::Trouble::Other))?; | 265 | let me = hello().map_err(|_| live::share::Refusal::Unreachable(live::Trouble::Other))?; |
| 246 | live::share::join(me, code, password, reach(), relay().as_deref()) | 266 | live::share::join_while(me, code, password, reach(), relay().as_deref(), waiting) |
| 247 | } | 267 | } |
| 248 | 268 | ||
| 249 | /// Keeps `welcome` as the notebook this computer joined: its location. | 269 | /// Keeps `welcome` as the notebook this computer joined: its location. |
| ... | @@ -259,6 +279,7 @@ pub(crate) fn joined(cache: &Path, welcome: live::wire::Welcome) -> io::Result<S | ... | @@ -259,6 +279,7 @@ pub(crate) fn joined(cache: &Path, welcome: live::wire::Welcome) -> io::Result<S |
| 259 | notebook: welcome.notebook, | 279 | notebook: welcome.notebook, |
| 260 | host: welcome.host, | 280 | host: welcome.host, |
| 261 | listed: false, | 281 | listed: false, |
| 282 | protocol: live::wire::VERSION, | ||
| 262 | }, | 283 | }, |
| 263 | ); | 284 | ); |
| 264 | keep(&file, &shares)?; | 285 | keep(&file, &shares)?; |
| ... | @@ -289,7 +310,7 @@ pub(crate) struct Peers { | ... | @@ -289,7 +310,7 @@ pub(crate) struct Peers { |
| 289 | /// The notebooks this computer shares, by location. | 310 | /// The notebooks this computer shares, by location. |
| 290 | pub(crate) hosts: BTreeMap<String, Arc<Host>>, | 311 | pub(crate) hosts: BTreeMap<String, Arc<Host>>, |
| 291 | /// Shares as kept for the next launch. | 312 | /// Shares as kept for the next launch. |
| 292 | sharing: Option<BTreeMap<String, Sharing>>, | 313 | sharing: Option<Arc<Mutex<BTreeMap<String, Sharing>>>>, |
| 293 | /// Shares starting on threads of their own, by location. | 314 | /// Shares starting on threads of their own, by location. |
| 294 | pub(crate) starting: HashMap<String, Option<String>>, | 315 | pub(crate) starting: HashMap<String, Option<String>>, |
| 295 | started: Option<Channel<(String, Result<Host, String>)>>, | 316 | started: Option<Channel<(String, Result<Host, String>)>>, |
| ... | @@ -410,7 +431,9 @@ impl State { | ... | @@ -410,7 +431,9 @@ impl State { |
| 410 | let sharing = self | 431 | let sharing = self |
| 411 | .peers | 432 | .peers |
| 412 | .sharing | 433 | .sharing |
| 413 | .get_or_insert_with(|| read_kept(&kept(&cache, HOSTING))) | 434 | .get_or_insert_with(|| Arc::new(Mutex::new(read_kept(&kept(&cache, HOSTING))))) |
| 435 | .lock() | ||
| 436 | .unwrap() | ||
| 414 | .clone(); | 437 | .clone(); |
| 415 | let open: Vec<Arc<Library>> = self.notebooks.clone(); | 438 | let open: Vec<Arc<Library>> = self.notebooks.clone(); |
| 416 | for library in &open { | 439 | for library in &open { |
| ... | @@ -443,25 +466,29 @@ impl State { | ... | @@ -443,25 +466,29 @@ impl State { |
| 443 | for location in closed { | 466 | for location in closed { |
| 444 | self.stop_sharing(&location); | 467 | self.stop_sharing(&location); |
| 445 | } | 468 | } |
| 446 | let now: BTreeMap<String, Sharing> = (self.peers.hosts.iter()) | 469 | for host in self.peers.hosts.values() { |
| 447 | .map(|(location, host)| (location.clone(), host.sharing())) | 470 | host.code(); |
| 448 | .collect(); | 471 | } |
| 449 | let before: BTreeMap<String, Sharing> = (sharing.into_iter()) | 472 | if self.peers.share.is_none() |
| 450 | .filter(|(location, _)| { | 473 | && let Some(library) = open.iter().find(|library| { |
| 451 | !self.peers.starting.contains_key(location) | 474 | self.peers |
| 452 | && open.iter().any(|library| library.location == *location) | 475 | .hosts |
| 476 | .get(&library.location) | ||
| 477 | .is_some_and(|host| !host.requests().is_empty()) | ||
| 453 | }) | 478 | }) |
| 454 | .collect(); | 479 | { |
| 455 | if now != before { | 480 | self.open_live_share(Arc::clone(library)); |
| 456 | if let Err(error) = keep(&kept(&cache, HOSTING), &now) { | ||
| 457 | eprintln!("Keeping what this computer shares: {error}"); | ||
| 458 | } | ||
| 459 | self.peers.sharing = Some(now); | ||
| 460 | } | 481 | } |
| 461 | } | 482 | } |
| 462 | 483 | ||
| 463 | /// Shares `library` as `sharing` says, on a thread of its own. | 484 | /// Shares `library` as `sharing` says, on a thread of its own. |
| 464 | pub(crate) fn start_sharing(&mut self, library: &Arc<Library>, sharing: Sharing) { | 485 | pub(crate) fn start_sharing(&mut self, library: &Arc<Library>, sharing: Sharing) { |
| 486 | let file = kept(&self.cache, HOSTING); | ||
| 487 | let kept = Arc::clone( | ||
| 488 | self.peers | ||
| 489 | .sharing | ||
| 490 | .get_or_insert_with(|| Arc::new(Mutex::new(read_kept(&file)))), | ||
| 491 | ); | ||
| 465 | self.peers.starting.insert(library.location.clone(), None); | 492 | self.peers.starting.insert(library.location.clone(), None); |
| 466 | let (started, _) = self.peers.started.get_or_insert_with(mpsc::channel); | 493 | let (started, _) = self.peers.started.get_or_insert_with(mpsc::channel); |
| 467 | let (started, library, redraw) = | 494 | let (started, library, redraw) = |
| ... | @@ -470,6 +497,7 @@ impl State { | ... | @@ -470,6 +497,7 @@ impl State { |
| 470 | let host = (|| -> Result<Host, Box<dyn std::error::Error>> { | 497 | let host = (|| -> Result<Host, Box<dyn std::error::Error>> { |
| 471 | let storage = library.reopen()?.into_storage(); | 498 | let storage = library.reopen()?.into_storage(); |
| 472 | let told = redraw.clone(); | 499 | let told = redraw.clone(); |
| 500 | let location = library.location.clone(); | ||
| 473 | Ok(Host::start( | 501 | Ok(Host::start( |
| 474 | storage, | 502 | storage, |
| 475 | hello()?, | 503 | hello()?, |
| ... | @@ -478,6 +506,14 @@ impl State { | ... | @@ -478,6 +506,14 @@ impl State { |
| 478 | reach(), | 506 | reach(), |
| 479 | relay().as_deref(), | 507 | relay().as_deref(), |
| 480 | move || told.wake_by_ref(), | 508 | move || told.wake_by_ref(), |
| 509 | move |sharing| { | ||
| 510 | let mut shares = kept.lock().unwrap(); | ||
| 511 | let mut next = shares.clone(); | ||
| 512 | next.insert(location.clone(), sharing.clone()); | ||
| 513 | keep(&file, &next)?; | ||
| 514 | *shares = next; | ||
| 515 | Ok(()) | ||
| 516 | }, | ||
| 481 | )?) | 517 | )?) |
| 482 | })(); | 518 | })(); |
| 483 | let _ = started.send(( | 519 | let _ = started.send(( |
| ... | @@ -521,11 +557,12 @@ impl State { | ... | @@ -521,11 +557,12 @@ impl State { |
| 521 | crate::spawn(move || host.stop()); | 557 | crate::spawn(move || host.stop()); |
| 522 | } | 558 | } |
| 523 | // Kept, the share would start again on the next frame, as after a relaunch. | 559 | // Kept, the share would start again on the next frame, as after a relaunch. |
| 524 | if let Some(sharing) = &mut self.peers.sharing | 560 | if let Some(sharing) = &self.peers.sharing { |
| 525 | && sharing.remove(location).is_some() | 561 | let mut sharing = sharing.lock().unwrap(); |
| 526 | && let Err(error) = keep(&kept(&self.cache, HOSTING), sharing) | 562 | sharing.remove(location); |
| 527 | { | 563 | if let Err(error) = keep(&kept(&self.cache, HOSTING), &*sharing) { |
| 528 | eprintln!("Keeping what this computer shares: {error}"); | 564 | eprintln!("Keeping what this computer shares: {error}"); |
| 565 | } | ||
| 529 | } | 566 | } |
| 530 | } | 567 | } |
| 531 | 568 |
crates/snowbound/src/share.rs+136-10| ... | @@ -8,7 +8,11 @@ use notebook::live::{ | ... | @@ -8,7 +8,11 @@ use notebook::live::{ |
| 8 | share::{self, Refusal, Sharing}, | 8 | share::{self, Refusal, Sharing}, |
| 9 | wire::Welcome, | 9 | wire::Welcome, |
| 10 | }; | 10 | }; |
| 11 | use std::sync::{Arc, mpsc}; | 11 | use std::sync::{ |
| 12 | Arc, | ||
| 13 | atomic::{AtomicBool, Ordering}, | ||
| 14 | mpsc, | ||
| 15 | }; | ||
| 12 | use ui::{Anchor, Axis, Id, Spec, Ui, children, fill, fit, px}; | 16 | use ui::{Anchor, Axis, Id, Spec, Ui, children, fill, fit, px}; |
| 13 | use winit::keyboard::NamedKey; | 17 | use winit::keyboard::NamedKey; |
| 14 | 18 | ||
| ... | @@ -41,6 +45,9 @@ pub(crate) struct ShareDialog { | ... | @@ -41,6 +45,9 @@ pub(crate) struct ShareDialog { |
| 41 | library: Arc<Library>, | 45 | library: Arc<Library>, |
| 42 | protect: bool, | 46 | protect: bool, |
| 43 | password: String, | 47 | password: String, |
| 48 | approve: bool, | ||
| 49 | replies: crate::live::Channel<Result<(), String>>, | ||
| 50 | error: Option<String>, | ||
| 44 | /// What was copied last: the code, or the link. | 51 | /// What was copied last: the code, or the link. |
| 45 | copied: Option<&'static str>, | 52 | copied: Option<&'static str>, |
| 46 | } | 53 | } |
| ... | @@ -53,6 +60,14 @@ pub(crate) struct JoinDialog { | ... | @@ -53,6 +60,14 @@ pub(crate) struct JoinDialog { |
| 53 | password: String, | 60 | password: String, |
| 54 | status: Status, | 61 | status: Status, |
| 55 | replies: crate::live::Channel<Result<Welcome, Refusal>>, | 62 | replies: crate::live::Channel<Result<Welcome, Refusal>>, |
| 63 | alive: Arc<AtomicBool>, | ||
| 64 | approval: Arc<AtomicBool>, | ||
| 65 | } | ||
| 66 | |||
| 67 | impl Drop for JoinDialog { | ||
| 68 | fn drop(&mut self) { | ||
| 69 | self.alive.store(false, Ordering::Release); | ||
| 70 | } | ||
| 56 | } | 71 | } |
| 57 | 72 | ||
| 58 | enum Status { | 73 | enum Status { |
| ... | @@ -92,6 +107,9 @@ fn refusal(refusal: &Refusal) -> String { | ... | @@ -92,6 +107,9 @@ fn refusal(refusal: &Refusal) -> String { |
| 92 | Refusal::Busy => "The Live Share relay is busy. Try again in a minute.".into(), | 107 | Refusal::Busy => "The Live Share relay is busy. Try again in a minute.".into(), |
| 93 | Refusal::Unreachable(trouble) => unreachable(*trouble), | 108 | Refusal::Unreachable(trouble) => unreachable(*trouble), |
| 94 | Refusal::TimedOut => "The computer sharing didn’t answer. Try again.".into(), | 109 | Refusal::TimedOut => "The computer sharing didn’t answer. Try again.".into(), |
| 110 | Refusal::Declined => "The person sharing declined this request.".into(), | ||
| 111 | Refusal::Cancelled => "Joining cancelled.".into(), | ||
| 112 | Refusal::NotAdmitted => "The computer sharing couldn’t add this device. Ask the person sharing to check Live Share.".into(), | ||
| 95 | } | 113 | } |
| 96 | } | 114 | } |
| 97 | 115 | ||
| ... | @@ -267,6 +285,9 @@ impl State { | ... | @@ -267,6 +285,9 @@ impl State { |
| 267 | library, | 285 | library, |
| 268 | protect: false, | 286 | protect: false, |
| 269 | password: String::new(), | 287 | password: String::new(), |
| 288 | approve: true, | ||
| 289 | replies: mpsc::channel(), | ||
| 290 | error: None, | ||
| 270 | copied: None, | 291 | copied: None, |
| 271 | }); | 292 | }); |
| 272 | self.ui.open_popup(share_id()); | 293 | self.ui.open_popup(share_id()); |
| ... | @@ -289,6 +310,8 @@ impl State { | ... | @@ -289,6 +310,8 @@ impl State { |
| 289 | password: String::new(), | 310 | password: String::new(), |
| 290 | status: Status::Idle, | 311 | status: Status::Idle, |
| 291 | replies: mpsc::channel(), | 312 | replies: mpsc::channel(), |
| 313 | alive: Arc::new(AtomicBool::new(true)), | ||
| 314 | approval: Arc::new(AtomicBool::new(false)), | ||
| 292 | }); | 315 | }); |
| 293 | self.ui.open_popup(join_id()); | 316 | self.ui.open_popup(join_id()); |
| 294 | self.ui.focus_all(code_field()); | 317 | self.ui.focus_all(code_field()); |
| ... | @@ -307,6 +330,9 @@ impl State { | ... | @@ -307,6 +330,9 @@ impl State { |
| 307 | return; | 330 | return; |
| 308 | } | 331 | } |
| 309 | let location = dialog.library.location.clone(); | 332 | let location = dialog.library.location.clone(); |
| 333 | for result in dialog.replies.1.try_iter() { | ||
| 334 | dialog.error = result.err(); | ||
| 335 | } | ||
| 310 | let host = self.peers.hosts.get(&location).cloned(); | 336 | let host = self.peers.hosts.get(&location).cloned(); |
| 311 | let starting = self.peers.starting.get(&location).cloned(); | 337 | let starting = self.peers.starting.get(&location).cloned(); |
| 312 | let ui = &mut self.ui; | 338 | let ui = &mut self.ui; |
| ... | @@ -319,6 +345,7 @@ impl State { | ... | @@ -319,6 +345,7 @@ impl State { |
| 319 | frame(ui, share_id(), "Live Share"); | 345 | frame(ui, share_id(), "Live Share"); |
| 320 | text(ui, "notebook", &dialog.library.name, true); | 346 | text(ui, "notebook", &dialog.library.name, true); |
| 321 | let (mut start, mut stop, mut copy) = (false, false, None); | 347 | let (mut start, mut stop, mut copy) = (false, false, None); |
| 348 | let (mut approval, mut allow, mut decline, mut remove) = (None, None, None, None); | ||
| 322 | match (&host, &starting) { | 349 | match (&host, &starting) { |
| 323 | (Some(host), _) => { | 350 | (Some(host), _) => { |
| 324 | let code = host.code(); | 351 | let code = host.code(); |
| ... | @@ -390,30 +417,85 @@ impl State { | ... | @@ -390,30 +417,85 @@ impl State { |
| 390 | ), | 417 | ), |
| 391 | _ => {} | 418 | _ => {} |
| 392 | } | 419 | } |
| 420 | if ui::check_box(ui, "approve", "Ask before joining", host.sharing().approve) | ||
| 421 | .clicked | ||
| 422 | { | ||
| 423 | approval = Some(!host.sharing().approve); | ||
| 424 | } | ||
| 425 | for request in host.requests() { | ||
| 426 | ui.open( | ||
| 427 | ("request", request.peer), | ||
| 428 | Spec { | ||
| 429 | size: [fill(), children()], | ||
| 430 | gap: 8.0, | ||
| 431 | ..Spec::default() | ||
| 432 | }, | ||
| 433 | ); | ||
| 434 | ui.leaf( | ||
| 435 | "name", | ||
| 436 | Spec { | ||
| 437 | size: [fill(), fit()], | ||
| 438 | text: Some(&format!("{} wants to join", request.name)), | ||
| 439 | overflow: ui::Overflow::Wrap, | ||
| 440 | ..Spec::default() | ||
| 441 | }, | ||
| 442 | ); | ||
| 443 | if ui::button(ui, "allow", "Allow").clicked { | ||
| 444 | allow = Some(request.peer); | ||
| 445 | } | ||
| 446 | if ui::button(ui, "decline", "Decline").clicked { | ||
| 447 | decline = Some(request.peer); | ||
| 448 | } | ||
| 449 | ui.close(); | ||
| 450 | } | ||
| 393 | ui.leaf( | 451 | ui.leaf( |
| 394 | "people", | 452 | "people", |
| 395 | Spec { | 453 | Spec { |
| 396 | size: [fill(), px(theme.font_size * 2.0)], | 454 | size: [fill(), px(theme.font_size * 2.0)], |
| 397 | text: Some("Connected"), | 455 | text: Some("Devices"), |
| 398 | bold: true, | 456 | bold: true, |
| 399 | role: Some(Role::Heading), | 457 | role: Some(Role::Heading), |
| 400 | ..Spec::default() | 458 | ..Spec::default() |
| 401 | }, | 459 | }, |
| 402 | ); | 460 | ); |
| 403 | let guests = host.guests(); | 461 | let devices = host.devices(); |
| 404 | if guests.is_empty() { | 462 | if devices.is_empty() { |
| 405 | text(ui, "nobody", "No one has joined yet.", true); | 463 | text(ui, "nobody", "No one has joined yet.", true); |
| 406 | } | 464 | } |
| 407 | for guest in &guests { | 465 | for (device, connected) in &devices { |
| 466 | ui.open( | ||
| 467 | ("device", device.secret), | ||
| 468 | Spec { | ||
| 469 | size: [fill(), children()], | ||
| 470 | gap: 8.0, | ||
| 471 | ..Spec::default() | ||
| 472 | }, | ||
| 473 | ); | ||
| 408 | ui.leaf( | 474 | ui.leaf( |
| 409 | ("guest", guest.hello.peer), | 475 | "name", |
| 410 | Spec { | 476 | Spec { |
| 411 | size: [fill(), px(theme.font_size * 1.6)], | 477 | size: [fill(), px(theme.font_size * 1.6)], |
| 412 | text: Some(&guest.hello.name), | 478 | text: Some(&match &device.device { |
| 479 | Some(name) => format!("{} · {name}", device.name), | ||
| 480 | None => device.name.clone(), | ||
| 481 | }), | ||
| 413 | role: Some(Role::ListItem), | 482 | role: Some(Role::ListItem), |
| 414 | ..Spec::default() | 483 | ..Spec::default() |
| 415 | }, | 484 | }, |
| 416 | ); | 485 | ); |
| 486 | ui.leaf( | ||
| 487 | "connection", | ||
| 488 | Spec { | ||
| 489 | size: [fit(), px(theme.font_size * 1.6)], | ||
| 490 | text: Some(if *connected { "Connected" } else { "Offline" }), | ||
| 491 | color: Some(theme.text_dim), | ||
| 492 | ..Spec::default() | ||
| 493 | }, | ||
| 494 | ); | ||
| 495 | if ui::button(ui, "remove", "Remove").clicked { | ||
| 496 | remove = Some(device.secret); | ||
| 497 | } | ||
| 498 | ui.close(); | ||
| 417 | } | 499 | } |
| 418 | buttons(ui); | 500 | buttons(ui); |
| 419 | stop = ui::button(ui, "stop", "Stop Sharing").clicked; | 501 | stop = ui::button(ui, "stop", "Stop Sharing").clicked; |
| ... | @@ -436,6 +518,9 @@ impl State { | ... | @@ -436,6 +518,9 @@ impl State { |
| 436 | ui.focus_all(password_field()); | 518 | ui.focus_all(password_field()); |
| 437 | } | 519 | } |
| 438 | } | 520 | } |
| 521 | if ui::check_box(ui, "approve", "Ask before joining", dialog.approve).clicked { | ||
| 522 | dialog.approve = !dialog.approve; | ||
| 523 | } | ||
| 439 | if dialog.protect { | 524 | if dialog.protect { |
| 440 | labelled(ui, "Password:", |ui| { | 525 | labelled(ui, "Password:", |ui| { |
| 441 | let spec = field_spec(ui); | 526 | let spec = field_spec(ui); |
| ... | @@ -455,6 +540,9 @@ impl State { | ... | @@ -455,6 +540,9 @@ impl State { |
| 455 | let offered = host.is_none() && !matches!(starting, Some(None)); | 540 | let offered = host.is_none() && !matches!(starting, Some(None)); |
| 456 | let done = ui::button(ui, "done", "Done").clicked || entered && !offered; | 541 | let done = ui::button(ui, "done", "Done").clicked || entered && !offered; |
| 457 | ui.close(); | 542 | ui.close(); |
| 543 | if let Some(error) = &dialog.error { | ||
| 544 | status(ui, error); | ||
| 545 | } | ||
| 458 | ui.close(); | 546 | ui.close(); |
| 459 | if let Some((what, text)) = copy { | 547 | if let Some((what, text)) = copy { |
| 460 | dialog.copied = self.clipboard.set_text(text).is_ok().then_some(what); | 548 | dialog.copied = self.clipboard.set_text(text).is_ok().then_some(what); |
| ... | @@ -462,6 +550,27 @@ impl State { | ... | @@ -462,6 +550,27 @@ impl State { |
| 462 | if stop { | 550 | if stop { |
| 463 | self.stop_sharing(&location); | 551 | self.stop_sharing(&location); |
| 464 | } | 552 | } |
| 553 | if let Some(host) = host { | ||
| 554 | if let Some(peer) = decline { | ||
| 555 | host.decline(&peer); | ||
| 556 | } | ||
| 557 | if approval.is_some() || allow.is_some() || remove.is_some() { | ||
| 558 | let replies = dialog.replies.0.clone(); | ||
| 559 | let redraw = self.redraw.clone(); | ||
| 560 | crate::spawn(move || { | ||
| 561 | let result = if let Some(approve) = approval { | ||
| 562 | host.approve(approve) | ||
| 563 | } else if let Some(peer) = allow { | ||
| 564 | host.allow(&peer) | ||
| 565 | } else { | ||
| 566 | host.remove(&remove.unwrap()) | ||
| 567 | }; | ||
| 568 | let _ = replies | ||
| 569 | .send(result.map_err(|error| format!("Couldn’t change access: {error}"))); | ||
| 570 | redraw.wake(); | ||
| 571 | }); | ||
| 572 | } | ||
| 573 | } | ||
| 465 | if start && !(dialog.protect && dialog.password.is_empty()) { | 574 | if start && !(dialog.protect && dialog.password.is_empty()) { |
| 466 | let password = if dialog.protect { | 575 | let password = if dialog.protect { |
| 467 | dialog.password.clone() | 576 | dialog.password.clone() |
| ... | @@ -469,7 +578,10 @@ impl State { | ... | @@ -469,7 +578,10 @@ impl State { |
| 469 | String::new() | 578 | String::new() |
| 470 | }; | 579 | }; |
| 471 | match Sharing::new(&password) { | 580 | match Sharing::new(&password) { |
| 472 | Ok(sharing) => self.start_sharing(&dialog.library, sharing), | 581 | Ok(mut sharing) => { |
| 582 | sharing.approve = dialog.approve; | ||
| 583 | self.start_sharing(&dialog.library, sharing); | ||
| 584 | } | ||
| 473 | Err(error) => { | 585 | Err(error) => { |
| 474 | self.peers | 586 | self.peers |
| 475 | .starting | 587 | .starting |
| ... | @@ -539,7 +651,14 @@ impl State { | ... | @@ -539,7 +651,14 @@ impl State { |
| 539 | }); | 651 | }); |
| 540 | match &dialog.status { | 652 | match &dialog.status { |
| 541 | Status::Idle => {} | 653 | Status::Idle => {} |
| 542 | Status::Waiting => status(ui, "Connecting…"), | 654 | Status::Waiting => status( |
| 655 | ui, | ||
| 656 | if dialog.approval.load(Ordering::Acquire) { | ||
| 657 | "Needs approval" | ||
| 658 | } else { | ||
| 659 | "Connecting…" | ||
| 660 | }, | ||
| 661 | ), | ||
| 543 | Status::Failed(message) => status(ui, message), | 662 | Status::Failed(message) => status(ui, message), |
| 544 | } | 663 | } |
| 545 | buttons(ui); | 664 | buttons(ui); |
| ... | @@ -560,8 +679,15 @@ impl State { | ... | @@ -560,8 +679,15 @@ impl State { |
| 560 | dialog.status = Status::Waiting; | 679 | dialog.status = Status::Waiting; |
| 561 | let (replies, redraw) = (dialog.replies.0.clone(), self.redraw.clone()); | 680 | let (replies, redraw) = (dialog.replies.0.clone(), self.redraw.clone()); |
| 562 | let password = dialog.password.clone(); | 681 | let password = dialog.password.clone(); |
| 682 | let (alive, approval) = | ||
| 683 | (Arc::clone(&dialog.alive), Arc::clone(&dialog.approval)); | ||
| 563 | crate::spawn(move || { | 684 | crate::spawn(move || { |
| 564 | let reply = crate::live::join(&code, &password); | 685 | let reply = crate::live::join(&code, &password, |waiting| { |
| 686 | if approval.swap(waiting, Ordering::AcqRel) != waiting { | ||
| 687 | redraw.wake_by_ref(); | ||
| 688 | } | ||
| 689 | alive.load(Ordering::Acquire) | ||
| 690 | }); | ||
| 565 | let _ = replies.send(reply); | 691 | let _ = replies.send(reply); |
| 566 | redraw.wake(); | 692 | redraw.wake(); |
| 567 | }); | 693 | }); |
crates/snowbound/tests/replay.rs+2-1| ... | @@ -723,11 +723,12 @@ fn stop_sharing_ends_the_share() { | ... | @@ -723,11 +723,12 @@ fn stop_sharing_ends_the_share() { |
| 723 | let mut steps = vec!["modifiers command shift", "key p", "modifiers", "settle"]; | 723 | let mut steps = vec!["modifiers command shift", "key p", "modifiers", "settle"]; |
| 724 | steps.extend(["type Live Share", "settle", "key Enter", "settle"]); | 724 | steps.extend(["type Live Share", "settle", "key Enter", "settle"]); |
| 725 | steps.extend(["key Enter", "wait 1500", "accessibility shared"]); | 725 | steps.extend(["key Enter", "wait 1500", "accessibility shared"]); |
| 726 | // Copy, Copy Link, then Stop Sharing. | 726 | // Copy, Copy Link, Ask before joining, then Stop Sharing. |
| 727 | steps.extend([ | 727 | steps.extend([ |
| 728 | "key Tab", | 728 | "key Tab", |
| 729 | "key Tab", | 729 | "key Tab", |
| 730 | "key Tab", | 730 | "key Tab", |
| 731 | "key Tab", | ||
| 731 | "key Enter", | 732 | "key Enter", |
| 732 | "wait 1500", | 733 | "wait 1500", |
| 733 | "accessibility stopped", | 734 | "accessibility stopped", |