1use std::collections::HashMap;
57use std::fs::File;
58use std::path::{Path, PathBuf};
59use std::sync::OnceLock;
60use std::time::{SystemTime, UNIX_EPOCH};
61
62use memmap2::Mmap;
63use parking_lot::Mutex;
64use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
65use std::os::unix::fs::MetadataExt;
66
67pub const SHARD_MAGIC: u32 = 0x5A52414C;
69pub const SHARD_FORMAT_VERSION: u32 = 7;
83#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
85#[archive(check_bytes)]
86pub struct ShardHeader {
87 pub magic: u32,
89 pub format_version: u32,
91 pub zshrs_version: String,
93 pub pointer_width: u32,
95 pub built_at_secs: u64,
97}
98#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
100#[archive(check_bytes)]
101pub struct AutoloadEntry {
102 pub binary_mtime_at_cache: i64,
104 pub binary_len_at_cache: u64,
108 pub cached_at_secs: i64,
110 pub source_dir: String,
114 pub source_sha: [u8; 32],
119 pub chunk_blob: Vec<u8>,
121 pub resolved: Option<ResolvedLoad>,
126}
127
128#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone, PartialEq)]
132#[archive(check_bytes)]
133pub struct ResolvedLoad {
134 pub body: String,
136 pub registered: String,
139 pub dump_ksh: i32,
141 pub from_wordcode: bool,
143 pub parse_failed: bool,
145 pub ksh_style: bool,
148 pub stamps: Vec<FileStamp>,
151 pub zwc_lines: Vec<crate::zwc_lineno::PipeLine>,
154}
155
156#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone, PartialEq)]
158#[archive(check_bytes)]
159pub struct FileStamp {
160 pub path: String,
162 pub exists: bool,
164 pub mtime_ns: i64,
166 pub len: u64,
168}
169#[derive(Archive, RkyvDeserialize, RkyvSerialize, Debug, Clone)]
171#[archive(check_bytes)]
172pub struct AutoloadShard {
173 pub header: ShardHeader,
175 pub entries: HashMap<String, AutoloadEntry>,
177}
178pub struct MmappedShard {
180 _mmap: Mmap,
182 archived: *const ArchivedAutoloadShard,
184}
185
186unsafe impl Send for MmappedShard {}
187unsafe impl Sync for MmappedShard {}
188
189impl MmappedShard {
190 pub fn open(path: &Path) -> Option<Self> {
192 let file = File::open(path).ok()?;
193 let mmap = unsafe { Mmap::map(&file).ok()? };
194 let archived = rkyv::check_archived_root::<AutoloadShard>(&mmap[..]).ok()?;
195 let archived_ptr = archived as *const ArchivedAutoloadShard;
196 Some(Self {
197 _mmap: mmap,
198 archived: archived_ptr,
199 })
200 }
201
202 fn shard(&self) -> &ArchivedAutoloadShard {
203 unsafe { &*self.archived }
204 }
205
206 fn header_ok(&self) -> bool {
207 let h = &self.shard().header;
208 let magic: u32 = h.magic.into();
209 let fv: u32 = h.format_version.into();
210 let pw: u32 = h.pointer_width.into();
211 magic == SHARD_MAGIC
212 && fv == SHARD_FORMAT_VERSION
213 && pw as usize == std::mem::size_of::<usize>()
214 && h.zshrs_version.as_str() == env!("CARGO_PKG_VERSION")
215 }
216
217 fn lookup(&self, name: &str) -> Option<&ArchivedAutoloadEntry> {
218 self.shard().entries.get(name)
219 }
220}
221
222fn entry_binary_matches(entry: &ArchivedAutoloadEntry) -> bool {
229 let Some((mtime, len)) = current_binary_identity() else {
230 return false;
232 };
233 let cached_mtime: i64 = entry.binary_mtime_at_cache.into();
234 let cached_len: u64 = entry.binary_len_at_cache.into();
235 cached_mtime == mtime && cached_len == len
236}
237
238pub struct AutoloadCache {
240 path: PathBuf,
242 lock_path: PathBuf,
244 mmap: Mutex<Option<MmappedShard>>,
246 pending: Mutex<Vec<(String, Vec<u8>, String, [u8; 32])>>,
256 resolved_pending: Mutex<HashMap<String, ResolvedLoad>>,
259}
260
261const PENDING_FLUSH_MAX: usize = 256;
268
269impl AutoloadCache {
270 pub fn open(path: &Path) -> std::io::Result<Self> {
272 if let Some(parent) = path.parent() {
273 std::fs::create_dir_all(parent)?;
274 }
275 let parent = path.parent().unwrap_or_else(|| Path::new("/tmp"));
276 let lock_path = parent.join(format!(
277 "{}.lock",
278 path.file_name()
279 .and_then(|s| s.to_str())
280 .unwrap_or("autoloads.rkyv")
281 ));
282 Ok(Self {
283 path: path.to_path_buf(),
284 lock_path,
285 mmap: Mutex::new(None),
286 pending: Mutex::new(Vec::new()),
287 resolved_pending: Mutex::new(HashMap::new()),
288 })
289 }
290
291 fn ensure_mmap(&self) {
292 let mut guard = self.mmap.lock();
293 if guard.is_none() {
294 *guard = MmappedShard::open(&self.path);
295 }
296 }
297
298 fn invalidate_mmap(&self) {
299 let mut guard = self.mmap.lock();
300 *guard = None;
301 }
302 pub fn get(&self, name: &str) -> Option<Vec<u8>> {
308 if let Some(blob) = self.pending_lookup(name, None, None) {
311 return (!blob.is_empty()).then_some(blob);
312 }
313 self.ensure_mmap();
314 let guard = self.mmap.lock();
315 let shard = guard.as_ref()?;
316 if !shard.header_ok() {
317 return None;
318 }
319 let entry = shard.lookup(name)?;
320 if !entry_binary_matches(entry) {
321 return None;
322 }
323 (!entry.chunk_blob.is_empty()).then(|| entry.chunk_blob.as_slice().to_vec())
324 }
325
326 pub fn get_for_source(
332 &self,
333 name: &str,
334 source_dir: &str,
335 source_sha: &[u8; 32],
336 ) -> Option<Vec<u8>> {
337 if let Some(blob) = self.pending_lookup(name, Some(source_dir), Some(source_sha)) {
338 return Some(blob);
339 }
340 self.ensure_mmap();
341 let guard = self.mmap.lock();
342 let shard = guard.as_ref()?;
343 if !shard.header_ok() {
344 return None;
345 }
346 let entry = shard.lookup(name)?;
347 if !entry_binary_matches(entry) {
348 return None;
349 }
350 if entry.source_dir.as_str() != source_dir {
351 return None;
352 }
353 if entry.source_sha.as_slice() != source_sha.as_slice() {
354 return None;
355 }
356 Some(entry.chunk_blob.as_slice().to_vec())
357 }
358
359 fn owned_shard_for_write(&self) -> AutoloadShard {
362 match read_owned_shard(&self.path) {
363 Some(s)
364 if s.header.zshrs_version == env!("CARGO_PKG_VERSION")
365 && s.header.pointer_width as usize == std::mem::size_of::<usize>()
366 && s.header.format_version == SHARD_FORMAT_VERSION =>
367 {
368 s
369 }
370 _ => fresh_shard(),
371 }
372 }
373
374 pub fn put_one(
383 &self,
384 name: &str,
385 chunk_blob: Vec<u8>,
386 source_dir: &str,
387 source_sha: [u8; 32],
388 ) -> Result<(), String> {
389 {
390 let mut pending = self.pending.lock();
391 pending.push((
392 name.to_string(),
393 chunk_blob,
394 source_dir.to_string(),
395 source_sha,
396 ));
397 if pending.len() < PENDING_FLUSH_MAX {
398 return Ok(());
399 }
400 }
401 self.flush_pending()
402 }
403
404 pub fn flush_pending(&self) -> Result<(), String> {
419 let batch = {
420 let mut pending = self.pending.lock();
421 if pending.is_empty() && self.resolved_pending.lock().is_empty() {
424 return Ok(());
425 }
426 std::mem::take(&mut *pending)
427 };
428 let Some(lock) = try_acquire_lock(&self.lock_path) else {
429 self.requeue(batch);
430 return Ok(());
431 };
432 self.put_many_locked(&batch, lock)
433 }
434
435 fn requeue(&self, mut batch: Vec<(String, Vec<u8>, String, [u8; 32])>) {
440 let mut pending = self.pending.lock();
441 batch.append(&mut pending);
442 let excess = batch.len().saturating_sub(PENDING_FLUSH_MAX);
443 batch.drain(..excess);
444 *pending = batch;
445 }
446
447 fn pending_lookup(
454 &self,
455 name: &str,
456 source_dir: Option<&str>,
457 source_sha: Option<&[u8; 32]>,
458 ) -> Option<Vec<u8>> {
459 let pending = self.pending.lock();
460 pending.iter().rev().find_map(|(n, blob, dir, sha)| {
463 if n != name {
464 return None;
465 }
466 if source_dir.is_some_and(|d| d != dir) {
467 return None;
468 }
469 if source_sha.is_some_and(|s| s != sha) {
470 return None;
471 }
472 Some(blob.clone())
473 })
474 }
475
476 pub fn put_many(&self, entries: &[(String, Vec<u8>, String, [u8; 32])]) -> Result<(), String> {
483 if entries.is_empty() && self.resolved_pending.lock().is_empty() {
484 return Ok(());
485 }
486 let Some(lock) = acquire_lock(&self.lock_path) else {
487 return Ok(());
488 };
489 self.put_many_locked(entries, lock)
490 }
491
492 fn put_many_locked(
495 &self,
496 entries: &[(String, Vec<u8>, String, [u8; 32])],
497 _lock: nix::fcntl::Flock<File>,
498 ) -> Result<(), String> {
499 let mut shard = self.owned_shard_for_write();
500 let (bin_mtime, bin_len) = current_binary_identity().unwrap_or((0, 0));
501 let now = now_secs();
502 for (name, chunk_blob, source_dir, source_sha) in entries {
503 if chunk_blob.is_empty() && shard.entries.contains_key(name) {
509 continue;
510 }
511 let noted = self
512 .resolved_pending
513 .lock()
514 .remove(name)
515 .filter(|r| r.registered.is_empty() || registered_digest(r) == *source_sha);
516 let prior = shard
517 .entries
518 .get(name)
519 .filter(|e| e.source_dir == *source_dir && e.source_sha == *source_sha)
520 .and_then(|e| e.resolved.clone());
521 shard.entries.insert(
522 name.clone(),
523 AutoloadEntry {
524 binary_mtime_at_cache: bin_mtime,
525 binary_len_at_cache: bin_len,
526 cached_at_secs: now,
527 source_dir: source_dir.clone(),
528 source_sha: *source_sha,
529 chunk_blob: chunk_blob.clone(),
530 resolved: noted.or(prior),
531 },
532 );
533 }
534 for (name, r) in std::mem::take(&mut *self.resolved_pending.lock()) {
538 if let Some(e) = shard.entries.get_mut(&name) {
539 let same_dir = r
540 .stamps
541 .first()
542 .is_some_and(|s| s.path == format!("{}/{}", e.source_dir, name));
543 let fits = if r.registered.is_empty() {
546 e.resolved.is_none()
547 } else {
548 registered_digest(&r) == e.source_sha
549 };
550 if same_dir && fits {
551 e.resolved = Some(r);
552 }
553 }
554 }
555 shard.header.built_at_secs = now as u64;
556 write_shard_atomic(&self.path, &shard)?;
557 self.invalidate_mmap();
558 Ok(())
559 }
560
561 pub fn remove(&self, name: &str) -> Result<(), String> {
568 self.pending.lock().retain(|(n, _, _, _)| n != name);
571 self.resolved_pending.lock().remove(name);
572 let _lock = match acquire_lock(&self.lock_path) {
573 Some(l) => l,
574 None => return Ok(()),
575 };
576 let mut shard = self.owned_shard_for_write();
577 if shard.entries.remove(name).is_none() {
578 return Ok(());
579 }
580 shard.header.built_at_secs = now_secs() as u64;
581 write_shard_atomic(&self.path, &shard)?;
582 self.invalidate_mmap();
583 Ok(())
584 }
585 pub fn entry_count(&self) -> usize {
587 self.ensure_mmap();
588 let guard = self.mmap.lock();
589 guard.as_ref().map(|s| s.shard().entries.len()).unwrap_or(0)
590 }
591
592 pub fn cached_names(&self) -> std::collections::HashSet<String> {
596 self.ensure_mmap();
597 let guard = self.mmap.lock();
598 let Some(shard) = guard.as_ref() else {
599 return std::collections::HashSet::new();
600 };
601 shard
602 .shard()
603 .entries
604 .keys()
605 .map(|k| k.as_str().to_string())
606 .collect()
607 }
608 pub fn stats(&self) -> (i64, i64) {
610 self.ensure_mmap();
611 let guard = self.mmap.lock();
612 let Some(shard) = guard.as_ref() else {
613 return (0, 0);
614 };
615 let count = shard.shard().entries.len() as i64;
616 let bytes: i64 = shard
617 .shard()
618 .entries
619 .values()
620 .map(|e| e.chunk_blob.len() as i64)
621 .sum();
622 (count, bytes)
623 }
624 pub fn clear(&self) -> std::io::Result<()> {
626 let _lock = acquire_lock(&self.lock_path);
627 let res = match std::fs::remove_file(&self.path) {
628 Ok(()) => Ok(()),
629 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
630 Err(e) => Err(e),
631 };
632 crate::atomic_write::reap_orphan_temps(&self.path);
633 self.invalidate_mmap();
634 res
635 }
636}
637
638fn acquire_lock(path: &Path) -> Option<nix::fcntl::Flock<File>> {
639 let f = File::options()
640 .read(true)
641 .write(true)
642 .create(true)
643 .truncate(false)
644 .open(path)
645 .ok()?;
646 nix::fcntl::Flock::lock(f, nix::fcntl::FlockArg::LockExclusive).ok()
647}
648
649fn try_acquire_lock(path: &Path) -> Option<nix::fcntl::Flock<File>> {
652 let f = File::options()
653 .read(true)
654 .write(true)
655 .create(true)
656 .truncate(false)
657 .open(path)
658 .ok()?;
659 nix::fcntl::Flock::lock(f, nix::fcntl::FlockArg::LockExclusiveNonblock).ok()
660}
661
662fn fresh_shard() -> AutoloadShard {
663 AutoloadShard {
664 header: ShardHeader {
665 magic: SHARD_MAGIC,
666 format_version: SHARD_FORMAT_VERSION,
667 zshrs_version: env!("CARGO_PKG_VERSION").to_string(),
668 pointer_width: std::mem::size_of::<usize>() as u32,
669 built_at_secs: now_secs() as u64,
670 },
671 entries: HashMap::new(),
672 }
673}
674
675fn read_owned_shard(path: &Path) -> Option<AutoloadShard> {
676 let bytes = std::fs::read(path).ok()?;
677 let archived = rkyv::check_archived_root::<AutoloadShard>(&bytes[..]).ok()?;
678 archived.deserialize(&mut rkyv::Infallible).ok()
679}
680
681fn write_shard_atomic(path: &Path, shard: &AutoloadShard) -> Result<(), String> {
682 let bytes = rkyv::to_bytes::<_, 4096>(shard).map_err(|e| format!("rkyv serialize: {}", e))?;
683 crate::atomic_write::write_bytes_atomic(path, &bytes)
684}
685
686fn now_secs() -> i64 {
687 SystemTime::now()
688 .duration_since(UNIX_EPOCH)
689 .map(|d| d.as_secs() as i64)
690 .unwrap_or(0)
691}
692
693fn current_binary_identity() -> Option<(i64, u64)> {
697 static BIN_ID: OnceLock<Option<(i64, u64)>> = OnceLock::new();
698 *BIN_ID.get_or_init(|| {
699 let exe = std::env::current_exe().ok()?;
700 let meta = std::fs::metadata(&exe).ok()?;
701 Some((meta.mtime(), meta.len()))
702 })
703}
704
705pub fn source_digest(text: &str) -> [u8; 32] {
712 use sha2::Digest;
713 let mut hasher = sha2::Sha256::new();
714 hasher.update(text.as_bytes());
715 hasher.finalize().into()
716}
717pub fn default_cache_path() -> PathBuf {
725 let root = if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
726 PathBuf::from(custom)
727 } else {
728 dirs::home_dir()
729 .unwrap_or_else(|| PathBuf::from("/tmp"))
730 .join(".zshrs")
731 };
732 root.join("autoloads.rkyv")
733}
734pub fn cache_enabled() -> bool {
739 if crate::extensions::script_cache::CACHE_DISABLED.load(std::sync::atomic::Ordering::Relaxed) {
740 return false;
741 }
742 !matches!(
743 std::env::var("ZSHRS_CACHE").as_deref(),
744 Ok("0") | Ok("false") | Ok("no")
745 )
746}
747pub static CACHE: once_cell::sync::Lazy<Option<AutoloadCache>> = once_cell::sync::Lazy::new(|| {
749 if !cache_enabled() {
750 return None;
751 }
752 AutoloadCache::open(&default_cache_path()).ok()
753});
754pub fn try_load(name: &str) -> Option<Vec<u8>> {
757 let cache = CACHE.as_ref()?;
758 cache.get(name)
759}
760
761pub fn try_load_for_source(name: &str, source_dir: &str, source_sha: &[u8; 32]) -> Option<Vec<u8>> {
764 let cache = CACHE.as_ref()?;
765 cache.get_for_source(name, source_dir, source_sha)
766}
767
768pub fn try_save_one(
770 name: &str,
771 chunk_blob: &[u8],
772 source_dir: &str,
773 source_sha: [u8; 32],
774) -> Result<(), String> {
775 let Some(cache) = CACHE.as_ref() else {
776 return Ok(());
777 };
778 cache.put_one(name, chunk_blob.to_vec(), source_dir, source_sha)
779}
780
781pub fn stamp_candidates(dir: &str, name: &str) -> Vec<FileStamp> {
786 [
787 format!("{dir}/{name}"),
788 format!("{dir}/{name}.zwc"),
789 format!("{dir}.zwc"),
790 ]
791 .into_iter()
792 .map(|path| match std::fs::metadata(&path) {
793 Ok(m) => FileStamp {
794 exists: true,
795 mtime_ns: m.mtime() * 1_000_000_000 + m.mtime_nsec(),
796 len: m.len(),
797 path,
798 },
799 Err(_) => FileStamp {
800 path,
801 exists: false,
802 mtime_ns: 0,
803 len: 0,
804 },
805 })
806 .collect()
807}
808
809fn registered_digest(r: &ResolvedLoad) -> [u8; 32] {
813 if r.from_wordcode {
814 source_digest(&format!("\0zwc-lines\0{}", r.registered))
815 } else {
816 source_digest(&r.registered)
817 }
818}
819
820fn stamps_fresh(stamps: &[FileStamp]) -> bool {
821 stamps.iter().all(|s| match std::fs::metadata(&s.path) {
822 Ok(m) => {
823 s.exists && m.mtime() * 1_000_000_000 + m.mtime_nsec() == s.mtime_ns && m.len() == s.len
824 }
825 Err(_) => !s.exists,
826 })
827}
828
829impl AutoloadCache {
830 fn resolve(&self, name: &str, fpath: &[String]) -> Option<(String, ResolvedLoad)> {
833 let pending_dir = self
834 .pending
835 .lock()
836 .iter()
837 .rev()
838 .find(|(n, ..)| n == name)
839 .map(|(_, _, dir, _)| dir.clone());
840 let (dir, resolved) = match pending_dir.and_then(|d| {
841 self.resolved_pending.lock().get(name).cloned().map(|r| (d, r))
842 }) {
843 Some(hit) => hit,
844 None => {
845 self.ensure_mmap();
846 let guard = self.mmap.lock();
847 let shard = guard.as_ref()?;
848 if !shard.header_ok() {
849 return None;
850 }
851 let entry = shard.lookup(name)?;
852 if !entry_binary_matches(entry) {
853 return None;
854 }
855 let resolved: ResolvedLoad =
856 entry.resolved.as_ref()?.deserialize(&mut rkyv::Infallible).ok()?;
857 (entry.source_dir.as_str().to_string(), resolved)
858 }
859 };
860 (fpath.iter().any(|d| *d == dir) && stamps_fresh(&resolved.stamps)).then_some((dir, resolved))
861 }
862}
863
864pub fn try_resolve(name: &str, fpath: &[String]) -> Option<(String, ResolvedLoad)> {
876 CACHE.as_ref()?.resolve(name, fpath).filter(|(_, r)| !r.registered.is_empty())
878}
879
880pub fn try_port_body(name: &str, fpath: &[String]) -> Option<(String, String)> {
885 let (dir, r) = CACHE.as_ref()?.resolve(name, fpath)?;
886 (!r.from_wordcode).then_some((dir, r.body))
887}
888
889pub fn record_port_probe(name: &str, dir: &str, body: &str) {
894 let Some(cache) = CACHE.as_ref() else {
895 return;
896 };
897 note_resolved(
898 name,
899 ResolvedLoad {
900 body: body.to_string(),
901 registered: String::new(),
902 dump_ksh: -1,
903 from_wordcode: false,
904 parse_failed: false,
905 ksh_style: false,
906 stamps: stamp_candidates(dir, name),
907 zwc_lines: Vec::new(),
908 },
909 );
910 let _ = cache.put_one(name, Vec::new(), dir, [0u8; 32]);
911}
912
913pub fn try_source_dir(name: &str, fpath: &[String]) -> Option<String> {
918 let cache = CACHE.as_ref()?;
919 if let Some((dir, _)) = cache.resolve(name, fpath) {
920 return Some(dir);
921 }
922 cache.ensure_mmap();
923 let guard = cache.mmap.lock();
924 let shard = guard.as_ref()?;
925 if !shard.header_ok() {
926 return None;
927 }
928 let dir = shard.lookup(name)?.source_dir.as_str().to_string();
929 let on_fpath = fpath.iter().any(|d| *d == dir);
930 (on_fpath && std::path::Path::new(&format!("{dir}/{name}")).exists()).then_some(dir)
931}
932
933pub fn note_resolved(name: &str, resolved: ResolvedLoad) {
936 if let Some(cache) = CACHE.as_ref() {
937 cache.resolved_pending.lock().insert(name.to_string(), resolved);
938 }
939}
940
941struct StagedSearch {
944 dir: String,
945 body: String,
946 dump_ksh: i32,
947 from_wordcode: bool,
948}
949
950thread_local! {
951 static STAGED_SEARCH: std::cell::RefCell<HashMap<String, StagedSearch>> =
952 std::cell::RefCell::new(HashMap::new());
953 static STAGED_HIT: std::cell::RefCell<HashMap<String, ResolvedLoad>> =
954 std::cell::RefCell::new(HashMap::new());
955}
956
957pub fn stage_search(name: &str, dir: &str, body: &str, dump_ksh: Option<i32>, from_wordcode: bool) {
959 STAGED_SEARCH.with(|s| {
960 s.borrow_mut().insert(
961 name.to_string(),
962 StagedSearch {
963 dir: dir.to_string(),
964 body: body.to_string(),
965 dump_ksh: dump_ksh.unwrap_or(-1),
966 from_wordcode,
967 },
968 )
969 });
970}
971
972pub fn commit_search(name: &str, body: &str, registered: &str, parse_failed: bool, ksh_style: bool) {
975 let Some(s) = STAGED_SEARCH.with(|s| s.borrow_mut().remove(name)) else {
976 return;
977 };
978 if s.body != body {
979 return;
980 }
981 let stamps = stamp_candidates(&s.dir, name);
982 let zwc_lines = if s.from_wordcode {
985 crate::zwc_lineno::autoload_lines(name).unwrap_or_default()
986 } else {
987 Vec::new()
988 };
989 note_resolved(
990 name,
991 ResolvedLoad {
992 body: s.body,
993 registered: registered.to_string(),
994 dump_ksh: s.dump_ksh,
995 from_wordcode: s.from_wordcode,
996 parse_failed,
997 ksh_style,
998 stamps,
999 zwc_lines,
1000 },
1001 );
1002}
1003
1004pub fn stage_hit(name: &str, resolved: ResolvedLoad) {
1006 STAGED_HIT.with(|s| s.borrow_mut().insert(name.to_string(), resolved));
1007}
1008
1009pub fn take_hit(name: &str, body: &str, ksh_style: bool) -> Option<(String, bool, bool)> {
1013 let r = STAGED_HIT.with(|s| s.borrow_mut().remove(name))?;
1014 (r.body == body && r.ksh_style == ksh_style).then_some((r.registered, r.from_wordcode, r.parse_failed))
1015}
1016
1017pub fn try_put_many(entries: &[(String, Vec<u8>, String, [u8; 32])]) -> Result<(), String> {
1020 let Some(cache) = CACHE.as_ref() else {
1021 return Ok(());
1022 };
1023 cache.put_many(entries)
1024}
1025
1026pub fn try_flush_pending() {
1032 if let Some(cache) = CACHE.as_ref() {
1033 if let Err(e) = cache.flush_pending() {
1034 if !crate::atexit_teardown::active() {
1038 tracing::warn!(error = %e, "autoload: could not flush cache");
1039 }
1040 }
1041 }
1042}
1043
1044extern "C" fn atexit_flush_pending() {
1067 crate::atexit_teardown::mark();
1068 let _ = std::panic::catch_unwind(try_flush_pending);
1069 let _ = std::panic::catch_unwind(crate::deparse_cache::try_flush_pending);
1071 let _ = std::panic::catch_unwind(crate::script_cache::try_flush_pending);
1072}
1073
1074pub fn install_atexit_flush() {
1076 static ONCE: std::sync::Once = std::sync::Once::new();
1077 ONCE.call_once(|| {
1078 unsafe {
1081 libc::atexit(atexit_flush_pending);
1082 }
1083 });
1084}
1085
1086pub fn try_remove(name: &str) {
1088 if let Some(cache) = CACHE.as_ref() {
1089 if let Err(e) = cache.remove(name) {
1090 tracing::warn!(name, error = %e, "autoload: could not drop bad cache entry");
1091 }
1092 }
1093}
1094
1095pub fn cached_names() -> std::collections::HashSet<String> {
1097 CACHE.as_ref().map(|c| c.cached_names()).unwrap_or_default()
1098}
1099pub fn entry_count() -> usize {
1101 CACHE.as_ref().map(|c| c.entry_count()).unwrap_or(0)
1102}
1103pub fn stats() -> Option<(i64, i64)> {
1105 CACHE.as_ref().map(|c| c.stats())
1106}
1107pub fn clear() -> bool {
1109 CACHE.as_ref().map(|c| c.clear().is_ok()).unwrap_or(false)
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114 use super::*;
1115 use tempfile::tempdir;
1116
1117 const DIR: &str = "/some/fpath/dir";
1118
1119 #[test]
1120 fn round_trip_one() {
1121 let _g = crate::test_util::global_state_lock();
1122 let dir = tempdir().unwrap();
1123 let cache_path = dir.path().join("autoloads.rkyv");
1124 let cache = AutoloadCache::open(&cache_path).unwrap();
1125 cache
1126 .put_one("foo", vec![1, 2, 3], DIR, source_digest("body"))
1127 .unwrap();
1128 assert_eq!(cache.get("foo"), Some(vec![1, 2, 3]));
1130 cache.flush_pending().unwrap();
1131 assert_eq!(cache.get("foo"), Some(vec![1, 2, 3]));
1132 assert_eq!(cache.entry_count(), 1);
1133 }
1134
1135 #[test]
1139 fn resolution_served_by_name_until_its_inputs_change() {
1140 let _g = crate::test_util::global_state_lock();
1141 let tmp = tempdir().unwrap();
1142 let fdir = tmp.path().join("fns");
1143 std::fs::create_dir(&fdir).unwrap();
1144 let file = fdir.join("_foo");
1145 std::fs::write(&file, "print one\n").unwrap();
1146 let fdir_s = fdir.to_string_lossy().to_string();
1147 let fpath = vec!["/elsewhere".to_string(), fdir_s.clone()];
1148 let resolved = ResolvedLoad {
1149 body: "print one\n".to_string(),
1150 registered: "_foo() {\nprint one\n}".to_string(),
1151 dump_ksh: -1,
1152 from_wordcode: false,
1153 parse_failed: false,
1154 ksh_style: false,
1155 stamps: stamp_candidates(&fdir_s, "_foo"),
1156 zwc_lines: Vec::new(),
1157 };
1158 let cache_path = tmp.path().join("autoloads.rkyv");
1159 let cache = AutoloadCache::open(&cache_path).unwrap();
1160 cache.resolved_pending.lock().insert("_foo".to_string(), resolved.clone());
1161 cache
1162 .put_one("_foo", vec![1], &fdir_s, registered_digest(&resolved))
1163 .unwrap();
1164 cache.flush_pending().unwrap();
1165
1166 let fresh = AutoloadCache::open(&cache_path).unwrap();
1167 assert_eq!(fresh.resolve("_foo", &fpath), Some((fdir_s.clone(), resolved)));
1168 assert_eq!(fresh.resolve("_foo", &["/elsewhere".to_string()]), None, "dir left fpath");
1169 std::fs::write(&file, "print two!\n").unwrap();
1170 assert_eq!(fresh.resolve("_foo", &fpath), None, "file edited");
1171 }
1172
1173 #[test]
1174 fn source_text_mismatch_is_a_miss() {
1175 let _g = crate::test_util::global_state_lock();
1179 let dir = tempdir().unwrap();
1180 let cache_path = dir.path().join("autoloads.rkyv");
1181 let cache = AutoloadCache::open(&cache_path).unwrap();
1182 let sha = source_digest("foo() {\nprint one\n}");
1183 cache.put_one("foo", vec![1, 2, 3], DIR, sha).unwrap();
1184 cache.flush_pending().unwrap();
1185 assert_eq!(cache.get_for_source("foo", DIR, &sha), Some(vec![1, 2, 3]));
1186 let edited = source_digest("foo() {\nprint two\n}");
1188 assert!(cache.get_for_source("foo", DIR, &edited).is_none());
1189 assert!(cache.get_for_source("foo", "/other/dir", &sha).is_none());
1191 }
1192
1193 #[test]
1194 fn an_entry_from_another_binary_is_never_served() {
1195 let _g = crate::test_util::global_state_lock();
1200 let dir = tempdir().unwrap();
1201 let cache_path = dir.path().join("autoloads.rkyv");
1202 let cache = AutoloadCache::open(&cache_path).unwrap();
1203 let sha = source_digest("foo() {\nprint one\n}");
1204 cache.put_one("foo", vec![1, 2, 3], DIR, sha).unwrap();
1205 cache.flush_pending().unwrap();
1206
1207 let mut shard = read_owned_shard(&cache_path).expect("shard readable");
1209 let entry = shard.entries.get_mut("foo").expect("entry present");
1210 entry.binary_mtime_at_cache += 10_000;
1211 write_shard_atomic(&cache_path, &shard).unwrap();
1212
1213 let reopened = AutoloadCache::open(&cache_path).unwrap();
1214 assert!(
1215 reopened.get_for_source("foo", DIR, &sha).is_none(),
1216 "a chunk from a newer build was accepted",
1217 );
1218 assert!(reopened.get("foo").is_none());
1219 }
1220
1221 #[test]
1222 fn remove_drops_the_entry() {
1223 let _g = crate::test_util::global_state_lock();
1224 let dir = tempdir().unwrap();
1225 let cache_path = dir.path().join("autoloads.rkyv");
1226 let cache = AutoloadCache::open(&cache_path).unwrap();
1227 let sha = source_digest("body");
1228 cache.put_one("foo", vec![1], DIR, sha).unwrap();
1229 cache.put_one("bar", vec![2], DIR, sha).unwrap();
1230 cache.remove("foo").unwrap();
1231 assert!(cache.get("foo").is_none());
1232 assert_eq!(cache.get("bar"), Some(vec![2]));
1233 }
1234
1235 #[test]
1236 fn cached_names_returns_keys() {
1237 let _g = crate::test_util::global_state_lock();
1238 let dir = tempdir().unwrap();
1239 let cache_path = dir.path().join("autoloads.rkyv");
1240 let cache = AutoloadCache::open(&cache_path).unwrap();
1241 let sha = source_digest("body");
1242 cache.put_one("alpha", vec![1], DIR, sha).unwrap();
1243 cache.put_one("beta", vec![2], DIR, sha).unwrap();
1244 cache.flush_pending().unwrap();
1245 let names = cache.cached_names();
1246 assert!(names.contains("alpha"));
1247 assert!(names.contains("beta"));
1248 assert_eq!(names.len(), 2);
1249 }
1250
1251 #[test]
1252 fn corrupt_shard_returns_none() {
1253 let _g = crate::test_util::global_state_lock();
1254 let dir = tempdir().unwrap();
1255 let cache_path = dir.path().join("autoloads.rkyv");
1256 std::fs::write(&cache_path, b"garbage").unwrap();
1257 let cache = AutoloadCache::open(&cache_path).unwrap();
1258 assert!(cache.get("anything").is_none());
1259 assert_eq!(cache.entry_count(), 0);
1260 }
1261
1262 #[test]
1263 fn clear_removes_file() {
1264 let _g = crate::test_util::global_state_lock();
1265 let dir = tempdir().unwrap();
1266 let cache_path = dir.path().join("autoloads.rkyv");
1267 let cache = AutoloadCache::open(&cache_path).unwrap();
1268 cache
1269 .put_one("x", vec![1], DIR, source_digest("body"))
1270 .unwrap();
1271 cache.flush_pending().unwrap();
1272 assert!(cache_path.exists());
1273 cache.clear().unwrap();
1274 assert!(!cache_path.exists());
1275 }
1276
1277 #[test]
1278 fn a_write_leaves_no_temp_file_behind() {
1279 let _g = crate::test_util::global_state_lock();
1282 let dir = tempdir().unwrap();
1283 let cache_path = dir.path().join("autoloads.rkyv");
1284 let cache = AutoloadCache::open(&cache_path).unwrap();
1285 cache
1286 .put_one("x", vec![1], DIR, source_digest("body"))
1287 .unwrap();
1288 cache.flush_pending().unwrap();
1290 let temps: Vec<_> = std::fs::read_dir(dir.path())
1291 .unwrap()
1292 .flatten()
1293 .map(|e| e.file_name().into_string().unwrap())
1294 .filter(|n| n.contains(".tmp."))
1295 .collect();
1296 assert!(temps.is_empty(), "temp files left behind: {temps:?}");
1297 }
1298
1299 #[test]
1300 fn put_one_defers_the_shard_write_until_flush() {
1301 let _g = crate::test_util::global_state_lock();
1304 let dir = tempdir().unwrap();
1305 let cache_path = dir.path().join("autoloads.rkyv");
1306 let cache = AutoloadCache::open(&cache_path).unwrap();
1307 let sha = source_digest("body");
1308
1309 for i in 0..8 {
1310 cache.put_one(&format!("f{i}"), vec![i], DIR, sha).unwrap();
1311 }
1312 assert!(
1313 !cache_path.exists(),
1314 "put_one wrote the shard before the flush boundary"
1315 );
1316 assert_eq!(cache.get_for_source("f3", DIR, &sha), Some(vec![3]));
1319
1320 cache.flush_pending().unwrap();
1321 assert_eq!(cache.entry_count(), 8);
1322 assert_eq!(cache.get_for_source("f7", DIR, &sha), Some(vec![7]));
1323 }
1324
1325 #[test]
1326 fn flush_preserves_entries_written_by_another_process() {
1327 let _g = crate::test_util::global_state_lock();
1330 let dir = tempdir().unwrap();
1331 let cache_path = dir.path().join("autoloads.rkyv");
1332 let sha = source_digest("body");
1333
1334 let peer = AutoloadCache::open(&cache_path).unwrap();
1335 peer.put_one("peer_fn", vec![9], DIR, sha).unwrap();
1336 peer.flush_pending().unwrap();
1337
1338 let mine = AutoloadCache::open(&cache_path).unwrap();
1339 mine.put_one("my_fn", vec![1], DIR, sha).unwrap();
1340 mine.flush_pending().unwrap();
1341
1342 assert_eq!(mine.get_for_source("peer_fn", DIR, &sha), Some(vec![9]));
1343 assert_eq!(mine.get_for_source("my_fn", DIR, &sha), Some(vec![1]));
1344 }
1345
1346 #[test]
1347 fn remove_drops_a_buffered_entry_before_it_reaches_disk() {
1348 let _g = crate::test_util::global_state_lock();
1352 let dir = tempdir().unwrap();
1353 let cache_path = dir.path().join("autoloads.rkyv");
1354 let cache = AutoloadCache::open(&cache_path).unwrap();
1355 let sha = source_digest("body");
1356
1357 cache.put_one("bad", vec![1], DIR, sha).unwrap();
1358 cache.remove("bad").unwrap();
1359 cache.flush_pending().unwrap();
1360
1361 assert!(cache.get("bad").is_none());
1362 let reopened = AutoloadCache::open(&cache_path).unwrap();
1363 assert!(reopened.get("bad").is_none());
1364 }
1365
1366 #[test]
1367 fn flush_with_the_lock_held_elsewhere_returns_and_keeps_the_batch() {
1368 let _g = crate::test_util::global_state_lock();
1372 let dir = tempdir().unwrap();
1373 let cache_path = dir.path().join("autoloads.rkyv");
1374 let cache = std::sync::Arc::new(AutoloadCache::open(&cache_path).unwrap());
1375 let sha = source_digest("body");
1376 cache.put_one("f", vec![1], DIR, sha).unwrap();
1377
1378 let held = acquire_lock(&cache.lock_path).unwrap();
1381 let (tx, rx) = std::sync::mpsc::channel();
1382 let flusher = std::sync::Arc::clone(&cache);
1383 std::thread::spawn(move || {
1386 let _ = tx.send(flusher.flush_pending());
1387 });
1388 let res = rx
1389 .recv_timeout(std::time::Duration::from_secs(5))
1390 .expect("flush_pending blocked on a lock held elsewhere");
1391 assert!(res.is_ok());
1392 assert!(!cache_path.exists(), "wrote the shard without the lock");
1393 assert_eq!(cache.get_for_source("f", DIR, &sha), Some(vec![1]));
1394
1395 drop(held);
1396 cache.flush_pending().unwrap();
1397 assert!(cache_path.exists());
1398 assert_eq!(cache.entry_count(), 1);
1399 }
1400}