From 07decfb49686f2911484344c35a283bbc2f6ed72 Mon Sep 17 00:00:00 2001
From: robobun <117481402+robobun@users.noreply.github.com>
Date: Fri, 2 Oct 2026 18:52:37 +0000
Subject: [PATCH 1/3] resolver: read every cached directory listing through one
reader, cache nothing for a failed read
The two listing loops in resolver.rs took an error from the directory
iterator as the end of the directory and cached the names read so far as
the whole directory. RealFS::readdir returned the error, and its callers
cached it with no generation.
RealFS::read_listing is now the only reader. A read that fails is tried
once more on a handle opened again by path. If it fails again, nothing is
stored: a fresh slot stays unknown and a stale listing keeps its names.
commit_listing is the only writer of EntriesOption::Entries.
A resolution that crossed a failed read fails with
'Cannot read directory "
": while resolving ""'.
---
src/resolver/lib.rs | 462 ++++++++++++++++++++++++++++++---------
src/resolver/resolver.rs | 337 ++++++++++++++--------------
2 files changed, 526 insertions(+), 273 deletions(-)
diff --git a/src/resolver/lib.rs b/src/resolver/lib.rs
index 25986a502f8d..2de470cdd695 100644
--- a/src/resolver/lib.rs
+++ b/src/resolver/lib.rs
@@ -967,7 +967,8 @@ pub mod fs {
bun_alloc::bss_map_inner! { pub entries_option_map : EntriesOption, 2048, true }
/// Resolver-side wrapper over `EntriesOptionMap` exposing the BSSMap surface
- /// (`get`, `get_or_put`, `at_index`, `put`, `mark_not_found`). ZST handle —
+ /// (`get`, `get_or_put`, `at_index`; `put` and `mark_not_found` stay inside
+ /// this module). ZST handle —
/// every call resolves to the `entries_option_map()` singleton; this keeps
/// `RealFS.entries` field-shaped without inlining the (large) backing array.
///
@@ -1019,7 +1020,9 @@ pub mod fs {
) -> Option<&mut EntriesOption> {
self.inner().at_index(index)
}
- pub(crate) fn put(
+ /// Private to this module: a listing reaches the cache through
+ /// `RealFS::commit_listing` only.
+ fn put(
&mut self,
result: &mut bun_alloc::Result,
value: EntriesOption,
@@ -1032,7 +1035,7 @@ pub mod fs {
.map(std::ptr::from_mut::)
.map_err(|_| crate::Error::Alloc(bun_alloc::AllocError))
}
- pub(crate) fn mark_not_found(&mut self, result: bun_alloc::Result) {
+ fn mark_not_found(&mut self, result: bun_alloc::Result) {
self.inner().mark_not_found(result)
}
pub(crate) fn remove(&mut self, key: &[u8]) -> bool {
@@ -1040,6 +1043,23 @@ pub mod fs {
}
}
+ /// A directory whose read failed, with the error. A failed read is cached
+ /// nowhere, so whoever needed the listing reports it.
+ pub struct DirReadFailure {
+ pub dir: Box<[u8]>,
+ pub err: crate::Error,
+ }
+
+ impl DirReadFailure {
+ #[cold]
+ pub(crate) fn new(dir: &[u8], err: crate::Error) -> Box {
+ Box::new(DirReadFailure {
+ dir: Box::from(strings::paths::without_trailing_slash_windows_path(dir)),
+ err,
+ })
+ }
+ }
+
/// The active filesystem backend (always the real filesystem).
pub type Implementation = RealFS;
@@ -1053,6 +1073,10 @@ pub mod fs {
/// this directly (`rfs.entries.get_or_put(..)`); modeled as the wrapper
/// `EntriesMap` (bun_alloc has no BSSMap equivalent).
pub entries: EntriesMap,
+ /// The entries of the last directory whose read failed, which
+ /// `read_listing` uses again when it reads that directory. Guarded by
+ /// `entries_mutex`.
+ failed_listing: Option,
pub(crate) cwd: &'static [u8],
#[cfg(not(windows))]
pub(crate) file_limit: usize,
@@ -1069,6 +1093,7 @@ pub mod fs {
RealFS {
entries_mutex: Mutex::default(),
entries: EntriesMap::new(),
+ failed_listing: None,
cwd,
#[cfg(not(windows))]
file_limit,
@@ -1143,39 +1168,234 @@ pub mod fs {
}
}
- /// Iterate `handle` and populate a
- /// fresh `DirEntry` (re-using `prev_map` Entry slots where the name matches).
+ /// Enumerate `handle` into `listing`, re-using `prev_map` Entry slots
+ /// where the name matches. This is the only loop that reads a directory
+ /// for the cache. An `Err` leaves `listing` with the names that were
+ /// read before it, which are not the directory.
fn readdir(
- &mut self,
- store_fd: bool,
+ listing: &mut DirEntry,
mut prev_map: Option<&mut dir_entry::EntryMap>,
- dir_: &'static [u8],
- generation: Generation,
handle: Fd,
- iterator: I,
- ) -> crate::CrateResult {
+ iterator: &I,
+ ) -> crate::CrateResult<()> {
let mut iter = bun_sys::iterate_dir(handle);
- let mut dir = DirEntry::init(dir_, generation);
-
- if store_fd {
- FileSystem::set_max_fd(bun_sys::Fd::native(handle));
- dir.fd = handle;
- }
-
let mut filename_store = FilenameStoreAppender::new();
while let Some(entry_) = iter.next()? {
- // debug("readdir entry {}", BStr::new(entry_.name.slice()));
- dir.add_entry_with_store(
+ listing.add_entry_with_store(
prev_map.as_deref_mut(),
&entry_,
&mut filename_store,
- &iterator,
+ iterator,
)?;
}
+ Ok(())
+ }
+
+ /// Read `handle` to the end and return the listing of `dir_path`.
+ /// Every listing that `commit_listing` publishes comes from here, so
+ /// the cache never holds the names of a read that stopped early.
+ ///
+ /// A read that fails is tried once more, on a handle opened again by
+ /// path, so a transient error does not reach the caller. When
+ /// `owns_handle`, that handle replaces `*handle` and the old one is
+ /// closed; otherwise `*handle` is left as it is. A non-void iterator
+ /// has already been handed entries, so its read is not tried again.
+ ///
+ /// On `Err` nothing is stored and the caller still owns `*handle`.
+ /// The caller holds `entries_mutex`.
+ pub(crate) fn read_listing(
+ &mut self,
+ in_place: Option<*mut DirEntry>,
+ dir_path: &[u8],
+ generation: Generation,
+ handle: &mut Fd,
+ owns_handle: bool,
+ store_fd: bool,
+ reserve: usize,
+ iterator: I,
+ ) -> crate::CrateResult {
+ let (dir, mut interned) = self.listing_dir(in_place, dir_path)?;
+ let mut listing = DirEntry::init(dir, generation);
+ listing.data.reserve(reserve);
+ if store_fd {
+ FileSystem::set_max_fd(bun_sys::Fd::native(*handle));
+ listing.fd = *handle;
+ }
+ let prev_map = match interned.as_mut() {
+ Some(failed) => Some(&mut failed.data),
+ // SAFETY: BSSMap-owned `DirEntry`; no aliasing here (`entries_mutex` held).
+ None => in_place.map(|p| unsafe { &mut (*p).data }),
+ };
+ let Err(err) = Self::readdir(&mut listing, prev_map, *handle, &iterator) else {
+ bun_core::scoped_log!(
+ crate::fs_full::Fs,
+ "readdir({}, {}) = {}",
+ *handle,
+ bstr::BStr::new(dir),
+ listing.data.count(),
+ );
+ return Ok(listing);
+ };
+ self.read_listing_again(
+ listing,
+ interned,
+ in_place,
+ err,
+ handle,
+ owns_handle,
+ store_fd,
+ I::IS_VOID,
+ )
+ }
+
+ /// The interned path for a new listing of `dir_path`, and the entries
+ /// that an earlier failed read of the same directory already interned.
+ fn listing_dir(
+ &mut self,
+ in_place: Option<*mut DirEntry>,
+ dir_path: &[u8],
+ ) -> crate::CrateResult<(&'static [u8], Option)> {
+ // SAFETY: `in_place` points to a `DirEntry` inside the BSSMap
+ // singleton; its `dir` field is DirnameStore-interned (&'static).
+ let stale_dir = in_place.map(|p| unsafe { (*p).dir });
+ let interned = match &self.failed_listing {
+ Some(failed)
+ if strings::paths::without_trailing_slash_windows_path(failed.dir)
+ == strings::paths::without_trailing_slash_windows_path(
+ stale_dir.unwrap_or(dir_path),
+ ) =>
+ {
+ self.failed_listing.take()
+ }
+ _ => None,
+ };
+ let dir = match (stale_dir, &interned) {
+ (Some(dir), _) => dir,
+ (None, Some(failed)) => failed.dir,
+ (None, None) => DirnameStore::instance().append_slice(dir_path)?,
+ };
+ Ok((dir, interned))
+ }
+
+ /// The cold half of `read_listing`: the read of `failed.dir` stopped
+ /// at `err` after `failed.data` was filled.
+ #[cold]
+ #[inline(never)]
+ fn read_listing_again(
+ &mut self,
+ mut failed: DirEntry,
+ interned: Option,
+ in_place: Option<*mut DirEntry>,
+ err: crate::Error,
+ handle: &mut Fd,
+ owns_handle: bool,
+ store_fd: bool,
+ try_again: bool,
+ ) -> crate::CrateResult {
+ // `EntryStore` and `FilenameStore` only grow. `failed.data` collects
+ // every entry interned for this directory, so that the next read of
+ // it, here or in a later lookup, interns only names it has not seen.
+ match &interned {
+ Some(earlier) => Self::keep_entries(&mut failed.data, &earlier.data)?,
+ None => {
+ if let Some(stale) = in_place {
+ // SAFETY: BSSMap-owned `DirEntry`; read-only here (`entries_mutex` held).
+ Self::keep_entries(&mut failed.data, unsafe { &(*stale).data })?;
+ }
+ }
+ }
+ drop(interned);
+
+ // The failed handle is not read again: its position is unknown.
+ if try_again {
+ if let Ok(fresh) = bun_sys::open_dir_for_iteration(Fd::cwd(), failed.dir) {
+ let mut listing = DirEntry::init(failed.dir, failed.generation);
+ listing.data.reserve(failed.data.count());
+ if Self::readdir(&mut listing, Some(&mut failed.data), fresh, &()).is_ok() {
+ if owns_handle {
+ let _ = bun_sys::close(*handle);
+ *handle = fresh;
+ FileSystem::set_max_fd(bun_sys::Fd::native(fresh));
+ } else {
+ let _ = bun_sys::close(fresh);
+ }
+ if store_fd {
+ listing.fd = *handle;
+ }
+ return Ok(listing);
+ }
+ let _ = bun_sys::close(fresh);
+ Self::keep_entries(&mut failed.data, &listing.data)?;
+ }
+ }
+
+ self.failed_listing = Some(failed);
+ Err(err)
+ }
+
+ /// Add to `into` the entries of `from` whose names it does not have.
+ fn keep_entries(
+ into: &mut dir_entry::EntryMap,
+ from: &dir_entry::EntryMap,
+ ) -> crate::CrateResult<()> {
+ for &entry in from.values() {
+ // SAFETY: `entry` is an `EntryStore` slot (never freed, never
+ // moved) and `base_lowercase_` is never mutated after construction.
+ let key: &'static [u8] =
+ unsafe { &*core::ptr::from_ref::<[u8]>((*entry).base_lowercase()) };
+ let hash = into.hash_key(key);
+ if into.get_hashed(hash, key).is_none() {
+ into.put_static_key_hashed(hash, key, entry)?;
+ }
+ }
+ Ok(())
+ }
+
+ /// Publish a listing that `read_listing` read to the end, or the empty
+ /// one of `opaque_listing`. This is the only writer of
+ /// `EntriesOption::Entries`. The caller holds `entries_mutex`.
+ pub(crate) fn commit_listing(
+ &mut self,
+ slot: &mut bun_alloc::Result,
+ in_place: Option<*mut DirEntry>,
+ listing: DirEntry,
+ ) -> crate::CrateResult<*mut EntriesOption> {
+ // `EntriesOption::Entries` holds an unbounded `&mut DirEntry` (raw,
+ // BSSMap-stored pointer), so a fresh slot is a leaked `Box`
+ // whose lifetime is the `entries_option_map()` singleton (process-static).
+ let entries_ptr: *mut DirEntry = match in_place {
+ Some(p) => {
+ // SAFETY: `p` is a live BSSMap slot, exclusively owned here
+ // under `entries_mutex`. The assignment drops the stale map.
+ unsafe { *p = listing };
+ p
+ }
+ None => bun_core::heap::into_raw(Box::new(listing)),
+ };
+ self.entries.put(
+ slot,
+ // SAFETY: see above; re-borrowed as 'static for the BSSMap slot.
+ EntriesOption::Entries(unsafe { &mut *entries_ptr }),
+ )
+ }
- // debug("readdir({}, {}) = {}", handle, dir_, dir.data.count());
+ /// The listing of a directory that this process may pass through but
+ /// not list: it has no entries.
+ pub(crate) fn opaque_listing(
+ &mut self,
+ in_place: Option<*mut DirEntry>,
+ dir_path: &[u8],
+ generation: Generation,
+ ) -> crate::CrateResult {
+ let (dir, _) = self.listing_dir(in_place, dir_path)?;
+ Ok(DirEntry::init(dir, generation))
+ }
- Ok(dir)
+ /// Cache that `dir` does not exist.
+ pub(crate) fn mark_dir_not_found(&mut self, dir: &[u8]) -> crate::CrateResult<()> {
+ let result = self.entries.get_or_put(dir)?;
+ self.entries.mark_not_found(result);
+ Ok(())
}
/// Cache (or threadlocal-
@@ -1206,6 +1426,41 @@ pub mod fs {
Ok(unsafe { &mut *opt })
}
+ /// What a read of `dir` that failed leaves behind. ENOENT and ENOTDIR
+ /// say that the directory went away while it was open, which ends like
+ /// a failed open. Any other error stores nothing: a fresh slot stays
+ /// unknown and a stale listing keeps its names and its generation, so
+ /// the next lookup reads the directory again. The caller gets the
+ /// error in the threadlocal slot, and in `read_failure` when it has
+ /// a place to report it.
+ fn read_failed(
+ &mut self,
+ dir: &[u8],
+ in_place: Option<*mut DirEntry>,
+ err: crate::Error,
+ read_failure: Option<&mut Option>>,
+ ) -> crate::CrateResult<&'static mut EntriesOption> {
+ if matches!(
+ err,
+ crate::Error::Sys(bun_errno::SystemErrno::ENOENT | bun_errno::SystemErrno::ENOTDIR)
+ ) {
+ if let Some(existing) = in_place {
+ // SAFETY: BSSMap-owned `DirEntry`; `entries_mutex` held.
+ unsafe { (*existing).data.clear() };
+ }
+ return self.read_directory_error(dir, err);
+ }
+ if let Some(read_failure) = read_failure {
+ *read_failure = Some(DirReadFailure::new(dir, err));
+ }
+ Ok(temp_entries_option_write(EntriesOption::Err(
+ dir_entry::Err {
+ original_err: err,
+ canonical_error: err,
+ },
+ )))
+ }
+
pub fn read_directory(
&mut self,
dir_: &[u8],
@@ -1216,6 +1471,19 @@ pub mod fs {
self.read_directory_with_iterator(dir_, handle_, generation, store_fd, ())
}
+ /// `read_directory` for a resolution. A read that fails is cached
+ /// nowhere, so the resolution reports it: it gets the directory and
+ /// the error in `read_failure`.
+ pub(crate) fn read_directory_for_resolution(
+ &mut self,
+ dir_: &[u8],
+ generation: Generation,
+ store_fd: bool,
+ read_failure: &mut Option>,
+ ) -> crate::CrateResult<&'static mut EntriesOption> {
+ self.read_directory_impl(dir_, None, generation, store_fd, (), Some(read_failure))
+ }
+
// One of the learnings here
//
// Closing file descriptors yields significant performance benefits on Linux
@@ -1231,6 +1499,25 @@ pub mod fs {
generation: Generation,
store_fd: bool,
iterator: I,
+ ) -> crate::CrateResult<&'static mut EntriesOption> {
+ self.read_directory_impl(
+ dir_maybe_trail_slash,
+ maybe_handle,
+ generation,
+ store_fd,
+ iterator,
+ None,
+ )
+ }
+
+ fn read_directory_impl(
+ &mut self,
+ dir_maybe_trail_slash: &[u8],
+ maybe_handle: Option,
+ generation: Generation,
+ store_fd: bool,
+ iterator: I,
+ read_failure: Option<&mut Option>>,
) -> crate::CrateResult<&'static mut EntriesOption> {
let dir = strings::paths::without_trailing_slash_windows_path(dir_maybe_trail_slash);
@@ -1267,82 +1554,51 @@ pub mod fs {
}
let had_handle = maybe_handle.is_some();
- let handle: Fd = match maybe_handle {
+ let mut handle: Fd = match maybe_handle {
Some(h) => h,
None => match self.open_dir(dir) {
Ok(h) => h,
Err(err) => return self.read_directory_error(dir, err),
},
};
-
- // Close the handle on every exit path. Use
- // scopeguard so close happens even if `readdir`/`put` early-return with `?`.
let should_close_handle = !had_handle && (!store_fd || self.need_to_close_files());
- let _close_guard = scopeguard::guard(handle, move |h| {
- if should_close_handle {
- let _ = bun_sys::close(h);
- }
- });
-
- // if we get this far, it's a real directory, so we can just store the dir name.
- // An in-place refresh always keeps the slot's existing interned name: callers
- // spell the same directory with and without a trailing slash, and rewriting
- // `dir` to the other spelling races every unlocked `Entry::dir()` reader.
- let dir: &'static [u8] = if let Some(existing) = in_place {
- // SAFETY: `in_place` points to a `DirEntry` inside the BSSMap singleton;
- // its `dir` field is DirnameStore-interned (&'static).
- unsafe { (*existing).dir }
- } else if !had_handle {
- DirnameStore::instance().append_slice(dir_maybe_trail_slash)?
- } else {
- // Intern into DirnameStore so the cache entry never dangles —
- // `append_slice` is a bump-pointer copy, cost is bounded.
- DirnameStore::instance().append_slice(dir)?
- };
- // Cache miss: read the directory entries
- let prev = in_place.map(|p| {
- // SAFETY: BSSMap-owned, no aliasing here (entries_mutex held).
- unsafe { &mut (*p).data }
- });
- let mut entries = match self.readdir(store_fd, prev, dir, generation, handle, iterator)
- {
- Ok(e) => e,
+ // if we get this far, it's a real directory, so `read_listing` can
+ // store the dir name. An in-place refresh always keeps the slot's
+ // existing interned name: callers spell the same directory with and
+ // without a trailing slash, and rewriting `dir` to the other spelling
+ // races every unlocked `Entry::dir()` reader.
+ let listing = match self.read_listing(
+ in_place,
+ if had_handle {
+ dir
+ } else {
+ dir_maybe_trail_slash
+ },
+ generation,
+ &mut handle,
+ !had_handle,
+ store_fd,
+ 0,
+ iterator,
+ ) {
+ Ok(listing) => listing,
Err(err) => {
- if let Some(existing) = in_place {
- // SAFETY: see above.
- unsafe { (*existing).data.clear() };
+ // No listing holds the handle. A non-void iterator was
+ // handed it with each entry when `store_fd`, so it stays open.
+ if !had_handle && (I::IS_VOID || !store_fd) {
+ let _ = bun_sys::close(handle);
}
- return self.read_directory_error(dir, err);
+ return self.read_failed(dir, in_place, err, read_failure);
}
};
- // `EntriesOption::Entries` here holds an unbounded `&mut DirEntry` (raw, BSSMap-stored
- // pointer), so a fresh slot is a leaked `Box` whose lifetime is the
- // `entries_option_map()` singleton (process-static).
- let entries_ptr: *mut DirEntry = match in_place {
- Some(p) => p,
- None => bun_core::heap::into_raw(Box::new(DirEntry::init(dir, generation))),
- };
- if let Some(original) = in_place {
- // SAFETY: BSSMap-owned; entries_mutex held.
- unsafe { (*original).data.clear() };
- }
- if store_fd && !entries.fd.is_valid() {
- entries.fd = handle;
+ let out = self.commit_listing(&mut cache_result, in_place, listing);
+ if should_close_handle {
+ let _ = bun_sys::close(handle);
}
-
- // SAFETY: `entries_ptr` is either a live BSSMap slot (`in_place`) or a fresh
- // leaked Box; exclusively owned here under `entries_mutex`.
- unsafe { *entries_ptr = entries };
- let result = EntriesOption::Entries(
- // SAFETY: see above — re-borrow as 'static for the BSSMap slot.
- unsafe { &mut *entries_ptr },
- );
-
- let out = self.entries.put(&mut cache_result, result)?;
// SAFETY: BSSMap-owned slot; outlives caller (process-static singleton).
- Ok(unsafe { &mut *out })
+ Ok(unsafe { &mut *out? })
}
/// Evicts `file_path` from the directory-entry cache; returns whether
@@ -1592,7 +1848,8 @@ pub mod fs {
/// stale. The generation-stale branch drops the existing `DirEntry`
/// (and the bucket allocation behind its `data` map) in place, and
/// every `.data` reader holds `entries_mutex` for the probe, so the
- /// caller must already hold it (the mutex is non-recursive).
+ /// caller must already hold it (the mutex is non-recursive). A re-read
+ /// that fails returns an `Err` and leaves the stale listing in place.
pub(crate) fn entries_at_locked(
&mut self,
index: bun_alloc::IndexType,
@@ -1614,7 +1871,7 @@ pub mod fs {
let dir = unsafe { (*e_ptr).dir };
// `open_dir_for_iteration`, NOT `RealFS.openDir`. On POSIX
// the two diverge: `O_DIRECTORY` only vs `O_RDONLY|O_DIRECTORY`.
- let handle = match bun_sys::open_dir_for_iteration(Fd::cwd(), dir) {
+ let mut handle = match bun_sys::open_dir_for_iteration(Fd::cwd(), dir) {
Ok(h) => h,
Err(err) => {
// SAFETY: see above.
@@ -1622,23 +1879,22 @@ pub mod fs {
return self.read_directory_error(dir, err.into()).ok();
}
};
- let _close_guard = scopeguard::guard(handle, |h| {
- let _ = bun_sys::close(h);
- });
- // SAFETY: see above — exclusive `&mut` on the prev map for the duration of `readdir`.
- let prev = Some(unsafe { &mut (*e_ptr).data });
- match self.readdir(false, prev, dir, generation, handle, ()) {
- Ok(new_entry) => {
- // SAFETY: see above.
- unsafe { (*e_ptr).data.clear() };
- // SAFETY: see above — slot is exclusively owned here.
- unsafe { *e_ptr = new_entry };
- }
- Err(err) => {
- // SAFETY: see above.
- unsafe { (*e_ptr).data.clear() };
- return self.read_directory_error(dir, err).ok();
- }
+ let read = self.read_listing(
+ Some(e_ptr),
+ dir,
+ generation,
+ &mut handle,
+ true,
+ false,
+ 0,
+ (),
+ );
+ let _ = bun_sys::close(handle);
+ match read {
+ // SAFETY: see above — slot is exclusively owned here.
+ // The assignment drops the stale map.
+ Ok(listing) => unsafe { *e_ptr = listing },
+ Err(err) => return self.read_failed(dir, Some(e_ptr), err, None).ok(),
}
}
}
diff --git a/src/resolver/resolver.rs b/src/resolver/resolver.rs
index 3615a6efe7fe..1cdf0e9f52f2 100644
--- a/src/resolver/resolver.rs
+++ b/src/resolver/resolver.rs
@@ -255,7 +255,6 @@ use bun_sys::Fd as FD;
use bun_threading::Mutex;
use crate::fs as Fs;
-use crate::fs::FilenameStoreAppender;
use crate::node_fallbacks as NodeFallbackModules;
use crate::package_json::{BrowserMap, ESModule, PackageJSON};
use crate::tsconfig_json::TSConfigJSON;
@@ -492,6 +491,11 @@ pub struct Resolver<'a> {
pub caches: CacheSet,
pub generation: Generation,
+ /// A directory read that failed since `resolve_and_auto_install` last
+ /// looked. Nothing is cached for that directory, so the resolution that
+ /// needed its listing has to report the error.
+ pub(crate) dir_read_failure: Option>,
+
/// Auto-install backend. `bun_install::PackageManager` implements
/// [`AutoInstaller`]; the resolver only sees the trait object so it stays
/// below `bun_install` in the dep graph. `None` until the auto-install
@@ -627,6 +631,7 @@ impl<'a> Resolver<'a> {
watcher: from.watcher,
caches: CacheSet::init(),
generation: from.generation,
+ dir_read_failure: None,
package_manager: from.package_manager,
on_wake_package_manager: from.on_wake_package_manager,
env_loader: from.env_loader,
@@ -926,6 +931,7 @@ impl<'a> Resolver<'a> {
elapsed: 0,
watcher: None,
generation: 0,
+ dir_read_failure: None,
package_manager: None,
on_wake_package_manager: Default::default(),
env_loader: None,
@@ -1050,12 +1056,78 @@ impl<'a> Resolver<'a> {
// var tracing_start: i128 — unused; dropped.
+ #[inline]
pub fn resolve_and_auto_install(
&mut self,
source_dir: &[u8],
import_path: &[u8],
kind: ast::ImportKind,
global_cache: GlobalCache,
+ ) -> ResultUnion {
+ let result =
+ self.resolve_and_auto_install_once(source_dir, import_path, kind, global_cache);
+ if self.dir_read_failure.is_some() {
+ return self.resolve_after_dir_read_failure(
+ result,
+ source_dir,
+ import_path,
+ kind,
+ global_cache,
+ );
+ }
+ result
+ }
+
+ /// A directory read failed, and nothing is cached for that directory, so
+ /// `first` can rest on a directory that was not listed: a lookup that
+ /// needs the listing takes the error as "not there" and tries the next
+ /// place. The failure can also be one that a lookup outside a resolution
+ /// left behind. Resolve once more. If the read fails again, this
+ /// resolution did cross it, and the error is the result.
+ #[cold]
+ #[inline(never)]
+ fn resolve_after_dir_read_failure(
+ &mut self,
+ first: ResultUnion,
+ source_dir: &[u8],
+ import_path: &[u8],
+ kind: ast::ImportKind,
+ global_cache: GlobalCache,
+ ) -> ResultUnion {
+ drop(first);
+ self.dir_read_failure = None;
+ let result =
+ self.resolve_and_auto_install_once(source_dir, import_path, kind, global_cache);
+ let Some(failure) = self.dir_read_failure.take() else {
+ return result;
+ };
+ drop(result);
+ self.log_mut().add_resolve_error(
+ None,
+ bun_ast::Range::NONE,
+ format_args!(
+ "Cannot read directory \"{}\": {} while resolving \"{}\"",
+ bstr::BStr::new(&failure.dir),
+ bstr::BStr::new(failure.err.name()),
+ bstr::BStr::new(import_path)
+ ),
+ import_path,
+ kind,
+ bun_ast::Error::ModuleNotFound,
+ );
+ ResultUnion::Failure(failure.err)
+ }
+
+ /// One pass of `resolve_and_auto_install`, without its check for a
+ /// directory read that failed. The check and its second pass both call
+ /// this one copy.
+ #[inline(never)]
+ fn resolve_and_auto_install_once(
+ &mut self,
+ source_dir: &[u8],
+ import_path: &[u8],
+ kind: ast::ImportKind,
+ global_cache: GlobalCache,
) -> ResultUnion {
// SAFETY: `import_path` is caller-interned (source text / DirnameStore)
// and outlives the returned Result.
@@ -3339,7 +3411,7 @@ impl<'a> Resolver<'a> {
core::ptr::null_mut();
let mut needs_iter = true;
let mut in_place: Option<*mut Fs::file_system::DirEntry> = None;
- let open_dir = match bun_sys::open_dir_for_iteration(FD::cwd(), dir_path) {
+ let mut open_dir = match bun_sys::open_dir_for_iteration(FD::cwd(), dir_path) {
Ok(d) => d,
Err(err) => {
// TODO: handle this error better
@@ -3364,83 +3436,29 @@ impl<'a> Resolver<'a> {
}
if needs_iter {
- // SAFETY: (block-wide) `in_place`/`dir_entries_ptr`/`dir_entries_option` point to slots
- // in `rfs.entries` (BSSMap singleton) or a fresh leaked Box; both outlive this fn and
- // are accessed under `rfs.entries_mutex` (see LIFETIMES.tsv).
- let mut new_entry = Fs::file_system::DirEntry::init(
- if let Some(existing) = in_place {
- // SAFETY: see block-wide note above.
- unsafe { &*existing }.dir
- } else {
- Fs::file_system::DirnameStore::instance()
- .append_slice(dir_path)
- .expect("unreachable")
- },
- self.generation,
- );
-
- // Pre-size `data` so the per-entry inserts below skip the
+ // Pre-size the listing so the per-entry inserts skip the
// 1→2→4→…→N hashbrown rehash cascade from an empty table. 64
// covers a typical node_modules package dir; larger dirs still
// rehash from there (cheap relative to starting at 0).
- new_entry.data.reserve(64);
-
- let mut dir_iterator = bun_sys::iterate_dir(open_dir);
- // Hoist the `FilenameStore` singleton resolve out of the per-entry loop
- // (see `DirEntry::add_entry` doc-comment) and reuse the appender state.
- let mut filename_store = FilenameStoreAppender::new();
- while let Ok(Some(_value)) = dir_iterator.next() {
- new_entry
- .add_entry_with_store(
- // SAFETY: see block-wide note above.
- in_place.map(|existing| unsafe { &mut (*existing).data }),
- &_value,
- &mut filename_store,
- (),
- )
- .expect("unreachable");
- }
- if let Some(existing) = in_place {
- // SAFETY: see block-wide note above.
- // NOTE: `StringHashMap` (std::HashMap newtype)
- // has no separate `clear_and_free`; `clear()` drops all entries.
- unsafe { &mut *existing }.data.clear();
- }
-
- if self.store_fd {
- new_entry.fd = open_dir;
- }
- // NOTE: see `dir_info_cached_maybe_log` — `DirEntry.data` holds a `NonNull`,
- // so a zeroed slot is UB; box `new_entry` directly for the fresh case.
- let dir_entries_ptr = match in_place {
- Some(p) => {
- // SAFETY: dir_entries_ptr is a live BSSMap slot (`in_place`).
- unsafe { *p = new_entry };
- p
+ let listing = match rfs!().read_listing(
+ in_place,
+ dir_path,
+ self.generation,
+ &mut open_dir,
+ true,
+ self.store_fd,
+ 64,
+ (),
+ ) {
+ Ok(listing) => listing,
+ Err(err) => {
+ open_dir.close();
+ self.dir_read_failure = Some(Fs::DirReadFailure::new(dir_path, err));
+ return Err(err);
}
- None => bun_core::heap::into_raw(Box::new(new_entry)),
};
-
- bun_core::scoped_log!(
- crate::fs_full::Fs,
- "readdir({}, {}) = {}",
- open_dir,
- bstr::BStr::new(dir_path),
- // SAFETY: `dir_entries_ptr` is a live BSSMap slot (`in_place`) or a freshly
- // boxed entry (see block-wide note above).
- unsafe { (*dir_entries_ptr).data.count() },
- );
-
- dir_entries_option = rfs!()
- .entries
- .put(
- &mut cached_dir_entry_result,
- Fs::file_system::real_fs::EntriesOption::Entries(
- // SAFETY: `dir_entries_ptr` is a live BSSMap slot (`in_place`) or a freshly boxed entry.
- unsafe { &mut *dir_entries_ptr },
- ),
- )
- .expect("unreachable");
+ dir_entries_option =
+ rfs!().commit_listing(&mut cached_dir_entry_result, in_place, listing)?;
}
// We must initialize it as empty so that the result index is correct.
@@ -4429,7 +4447,7 @@ impl<'a> Resolver<'a> {
let queue_top_safe_path: &[u8] = qt_safe_path.slice();
queue_slice_len -= 1;
- let open_dir: FD = if queue_top.fd.is_valid() {
+ let mut open_dir: FD = if queue_top.fd.is_valid() {
queue_top.fd
} else {
'open_dir: {
@@ -4508,17 +4526,15 @@ impl<'a> Resolver<'a> {
);
break 'open_dir FD::INVALID;
}
- let cached_dir_entry_result = rfs!()
- .entries
- .get_or_put(queue_top_unsafe_path)
- .expect("unreachable");
// If we don't properly cache not found, then we repeatedly attempt to open the same directories,
// which causes a perf trace that looks like this stupidity;
//
// openat(dfd: CWD, filename: "node_modules/react", flags: RDONLY|DIRECTORY) = -1 ENOENT (No such file or directory)
// ...
self.dir_cache_mut().mark_not_found(queue_top.result);
- rfs!().entries.mark_not_found(cached_dir_entry_result);
+ rfs!()
+ .mark_dir_not_found(queue_top_unsafe_path)
+ .expect("unreachable");
if err != crate::Error::Sys(bun_errno::SystemErrno::ENOENT) {
if enable_logging {
let pretty = queue_top_unsafe_path;
@@ -4615,90 +4631,78 @@ impl<'a> Resolver<'a> {
}
if needs_iter {
- // SAFETY: (block-wide) `in_place`/`dir_entries_ptr`/`dir_entries_option` point to
- // slots in `rfs.entries` (BSSMap singleton) or a fresh leaked Box; both outlive this
- // fn and are accessed under `rfs.entries_mutex` (see LIFETIMES.tsv).
- let mut new_entry = Fs::file_system::DirEntry::init(
- if let Some(existing) = in_place {
- // SAFETY: see block-wide note above.
- unsafe { &*existing }.dir
- } else {
- Fs::file_system::DirnameStore::instance()
- .append_slice(dir_path)
- .expect("unreachable")
- },
- self.generation,
- );
-
- // Pre-size `data` so the per-entry inserts below skip the
- // 1→2→4→…→N hashbrown rehash cascade from an empty table. 64
- // covers a typical node_modules package dir; larger dirs
- // still rehash from there (cheap relative to starting at 0).
- new_entry.data.reserve(64);
-
// A permission-denied ancestor has no fd to enumerate; its
// entry set stays empty.
+ let mut listing = None;
if open_dir.is_valid() {
- let mut dir_iterator = bun_sys::iterate_dir(open_dir);
- // NOTE: `WrappedIterator::next` returns
- // `Result