| ... | ... | @@ -324,8 +324,7 @@ pub struct Library { |
| 324 | 324 | /// Sections opened ahead of being shown, or left open after, most recent first, which |
| 325 | 325 | /// `open` hands out before opening any. |
| 326 | 326 | kept: Mutex<crate::prefetch::Recent<String, Section>>, |
| 327 | | /// The notebook's style themes, and when they were read. |
| 328 | | themes: Mutex<Option<(Instant, Arc<Themes>)>>, |
| 327 | themes: Mutex<ThemeCache>, |
| 329 | 328 | /// The keys of its password-protected sections unlocked this run, kept as it is read again. |
| 330 | 329 | keys: Arc<Keys>, |
| 331 | 330 | /// The share a notebook another computer shares by Live Share is reached through. |
| ... | ... | @@ -338,6 +337,13 @@ pub struct Library { |
| 338 | 337 | #[derive(Default)] |
| 339 | 338 | pub struct Keys(Mutex<std::collections::HashMap<[u8; 16], (Key, Instant)>>); |
| 340 | 339 | |
| 340 | #[derive(Default)] |
| 341 | struct ThemeCache { |
| 342 | value: Arc<Themes>, |
| 343 | checked: Option<Instant>, |
| 344 | reading: bool, |
| 345 | } |
| 346 | |
| 341 | 347 | impl Library { |
| 342 | 348 | /// `location` named `name`, with no notebook read, server, sync or kept sections. |
| 343 | 349 | fn new(location: &str, name: String, cache: &Path) -> Self { |
| ... | ... | @@ -645,27 +651,53 @@ impl Library { |
| 645 | 651 | }); |
| 646 | 652 | } |
| 647 | 653 | |
| 648 | | /// The notebook's style themes, read again after a while so other machines' changes |
| 649 | | /// reach pages opened later; a section opened on its own has none. |
| 650 | | pub fn themes(&self) -> Arc<Themes> { |
| 654 | /// The notebook's last-read themes, refreshed off the frame thread. |
| 655 | pub fn themes(self: &Arc<Self>) -> Arc<Themes> { |
| 651 | 656 | let mut kept = self |
| 652 | 657 | .themes |
| 653 | 658 | .lock() |
| 654 | 659 | .unwrap_or_else(|poisoned| poisoned.into_inner()); |
| 655 | | if let Some((read, themes)) = &*kept |
| 656 | | && read.elapsed() < Duration::from_secs(30) |
| 660 | let themes = Arc::clone(&kept.value); |
| 661 | if !matches!(self.notebook, Ok(Some(_))) |
| 662 | || kept.reading |
| 663 | || kept |
| 664 | .checked |
| 665 | .is_some_and(|read| read.elapsed() < Duration::from_secs(30)) |
| 657 | 666 | { |
| 658 | | return Arc::clone(themes); |
| 667 | return themes; |
| 659 | 668 | } |
| 660 | | let themes = match &self.notebook { |
| 661 | | Ok(Some(notebook)) => notebook.themes().unwrap_or_else(|error| { |
| 662 | | eprintln!("{}: reading its themes failed: {error}", self.location); |
| 663 | | Themes::default() |
| 664 | | }), |
| 665 | | _ => Themes::default(), |
| 666 | | }; |
| 667 | | let themes = Arc::new(themes); |
| 668 | | *kept = Some((Instant::now(), Arc::clone(&themes))); |
| 669 | kept.reading = true; |
| 670 | drop(kept); |
| 671 | let (library, before) = (Arc::clone(self), Arc::clone(&themes)); |
| 672 | crate::spawn(move || { |
| 673 | let read = match &library.notebook { |
| 674 | Ok(Some(notebook)) => notebook.themes(), |
| 675 | _ => Ok(Themes::default()), |
| 676 | }; |
| 677 | let mut kept = library |
| 678 | .themes |
| 679 | .lock() |
| 680 | .unwrap_or_else(|poisoned| poisoned.into_inner()); |
| 681 | kept.reading = false; |
| 682 | kept.checked = Some(Instant::now()); |
| 683 | match read { |
| 684 | Ok(mut read) => { |
| 685 | // A local edit may have landed while the file was being read. |
| 686 | if !Arc::ptr_eq(&before, &kept.value) { |
| 687 | notebook::sidecar::themes::merge(&mut read, (*kept.value).clone()); |
| 688 | } |
| 689 | let changed = *kept.value != read; |
| 690 | kept.value = Arc::new(read); |
| 691 | drop(kept); |
| 692 | if changed { |
| 693 | notify_background(); |
| 694 | } |
| 695 | } |
| 696 | Err(error) => { |
| 697 | eprintln!("{}: reading its themes failed: {error}", library.location); |
| 698 | } |
| 699 | } |
| 700 | }); |
| 669 | 701 | themes |
| 670 | 702 | } |
| 671 | 703 | |
| ... | ... | @@ -675,13 +707,15 @@ impl Library { |
| 675 | 707 | if !matches!(self.notebook, Ok(Some(_))) { |
| 676 | 708 | return; |
| 677 | 709 | } |
| 678 | | let mut themes = (*self.themes()).clone(); |
| 679 | | notebook::sidecar::themes::merge(&mut themes, change.clone()); |
| 680 | | *self |
| 681 | | .themes |
| 682 | | .lock() |
| 683 | | .unwrap_or_else(|poisoned| poisoned.into_inner()) = |
| 684 | | Some((Instant::now(), Arc::new(themes))); |
| 710 | self.themes(); |
| 711 | { |
| 712 | let mut kept = self |
| 713 | .themes |
| 714 | .lock() |
| 715 | .unwrap_or_else(|poisoned| poisoned.into_inner()); |
| 716 | notebook::sidecar::themes::merge(Arc::make_mut(&mut kept.value), change.clone()); |
| 717 | kept.checked = Some(Instant::now()); |
| 718 | } |
| 685 | 719 | let library = Arc::clone(self); |
| 686 | 720 | crate::spawn(move || { |
| 687 | 721 | let kept = library |
| ... | ... | @@ -1471,6 +1505,89 @@ pub fn locate(path: &Path) -> Located { |
| 1471 | 1505 | mod tests { |
| 1472 | 1506 | use super::*; |
| 1473 | 1507 | |
| 1508 | #[cfg(unix)] |
| 1509 | #[test] |
| 1510 | fn stalled_theme_reads_leave_the_cache_usable_and_keep_local_edits() { |
| 1511 | use notebook::sidecar::themes::{Assignment, Scope, merge}; |
| 1512 | use std::{ |
| 1513 | io::Write, |
| 1514 | os::unix::{ffi::OsStrExt, fs::OpenOptionsExt}, |
| 1515 | }; |
| 1516 | |
| 1517 | let root = |
| 1518 | std::env::temp_dir().join(format!("snowbound-stalled-themes-{}", std::process::id())); |
| 1519 | let _ = std::fs::remove_dir_all(&root); |
| 1520 | std::fs::create_dir_all(root.join("notebook/.snowbound")).unwrap(); |
| 1521 | let path = root.join("notebook/.snowbound/themes.json"); |
| 1522 | let filename = std::ffi::CString::new(path.as_os_str().as_bytes()).unwrap(); |
| 1523 | assert_eq!(unsafe { libc::mkfifo(filename.as_ptr(), 0o600) }, 0); |
| 1524 | let library = Arc::new(Library::notebook( |
| 1525 | root.join("notebook").to_str().unwrap(), |
| 1526 | &root.join("cache"), |
| 1527 | )); |
| 1528 | let cached = |library: Arc<Library>| { |
| 1529 | let (send, receive) = std::sync::mpsc::channel(); |
| 1530 | std::thread::spawn(move || send.send(library.themes()).unwrap()); |
| 1531 | receive |
| 1532 | .recv_timeout(Duration::from_secs(1)) |
| 1533 | .expect("theme access must not wait for storage") |
| 1534 | }; |
| 1535 | assert_eq!(*cached(Arc::clone(&library)), Themes::default()); |
| 1536 | let deadline = Instant::now() + Duration::from_secs(3); |
| 1537 | let mut writer = loop { |
| 1538 | match std::fs::OpenOptions::new() |
| 1539 | .write(true) |
| 1540 | .custom_flags(libc::O_NONBLOCK) |
| 1541 | .open(&path) |
| 1542 | { |
| 1543 | Ok(writer) => break writer, |
| 1544 | Err(error) |
| 1545 | if error.raw_os_error() == Some(libc::ENXIO) && Instant::now() < deadline => |
| 1546 | { |
| 1547 | std::thread::sleep(Duration::from_millis(5)); |
| 1548 | } |
| 1549 | Err(error) => panic!("theme reader did not start: {error}"), |
| 1550 | } |
| 1551 | }; |
| 1552 | assert_eq!(*cached(Arc::clone(&library)), Themes::default()); |
| 1553 | let assignment = |theme: &str, assigned| Themes { |
| 1554 | assignments: vec![Assignment { |
| 1555 | scope: Scope::Notebook, |
| 1556 | theme: Some(theme.to_owned()), |
| 1557 | assigned, |
| 1558 | }], |
| 1559 | ..Themes::default() |
| 1560 | }; |
| 1561 | let local = assignment("local", 10); |
| 1562 | { |
| 1563 | let mut kept = library.themes.lock().unwrap(); |
| 1564 | merge(Arc::make_mut(&mut kept.value), local.clone()); |
| 1565 | } |
| 1566 | assert_eq!(*cached(Arc::clone(&library)), local); |
| 1567 | writer |
| 1568 | .write_all(&serde_json::to_vec(&assignment("remote", 5)).unwrap()) |
| 1569 | .unwrap(); |
| 1570 | drop(writer); |
| 1571 | while library.themes.lock().unwrap().reading { |
| 1572 | assert!(Instant::now() < deadline, "theme read did not finish"); |
| 1573 | std::thread::sleep(Duration::from_millis(5)); |
| 1574 | } |
| 1575 | assert_eq!(*cached(Arc::clone(&library)), local); |
| 1576 | std::fs::remove_file(&path).unwrap(); |
| 1577 | std::fs::create_dir(&path).unwrap(); |
| 1578 | library.themes.lock().unwrap().checked = Some(Instant::now() - Duration::from_secs(31)); |
| 1579 | assert_eq!(*cached(Arc::clone(&library)), local); |
| 1580 | while library.themes.lock().unwrap().reading { |
| 1581 | assert!( |
| 1582 | Instant::now() < deadline, |
| 1583 | "failed theme read did not finish" |
| 1584 | ); |
| 1585 | std::thread::sleep(Duration::from_millis(5)); |
| 1586 | } |
| 1587 | assert_eq!(*cached(library), local); |
| 1588 | std::fs::remove_dir_all(root).unwrap(); |
| 1589 | } |
| 1590 | |
| 1474 | 1591 | /// A section kept open is what `open` hands out next, without opening it again, and |
| 1475 | 1592 | /// readying a section another holds leaves it to that one at once. |
| 1476 | 1593 | #[test] |