Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/bun_alloc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2343,6 +2343,43 @@ impl<ValueType, const COUNT: usize, const REMOVE_TRAILING_SLASHES: bool>
let _key = Self::key_hash(denormalized_key);
self.index.remove(&_key).is_some()
}

/// Removes every present entry whose value satisfies `pred`. Not-found
/// markers carry no value and stay. Returns how many were removed. The
/// values stay allocated: the map never frees a slot.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub fn remove_where(&mut self, mut pred: impl FnMut(&ValueType) -> bool) -> usize {
let _guard = self.mutex.lock();
let mut doomed: Vec<HashKeyType> = Vec::new();
for (&hash, &index) in &self.index {
if index.index() == NOT_FOUND.index() || index.index() == UNASSIGNED.index() {
continue;
}
let value: &ValueType = if index.is_overflow() {
self.overflow_list.at_index_mut(index)
} else {
// SAFETY: a non-sentinel, non-overflow index was assigned by
// `put`, which initialized this slot via `.write()`.
unsafe { self.backing_buf[index.index() as usize].assume_init_ref() }
};
if pred(value) {
doomed.try_reserve(1).unwrap_or_else(|_| out_of_memory());
doomed.push(hash);
}
}
for hash in &doomed {
self.index.remove(hash);
}
doomed.len()
}

/// Drops every not-found marker. Returns how many there were.
pub fn clear_not_found(&mut self) -> usize {
let _guard = self.mutex.lock();
let before = self.index.len();
self.index
.retain(|_, index| index.index() != NOT_FOUND.index());
before - self.index.len()
}
}

// ──────────────────────────────────────────────────────────────────────────
Expand Down
158 changes: 158 additions & 0 deletions src/jsc/hot_reloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use bun_collections::StringSet;
use bun_core::Output;
use bun_core::ZStr;
#[cfg(not(windows))]
use bun_paths::PathBuffer;
#[cfg(not(windows))]
use bun_paths::SEP;
use bun_paths::strings;
#[cfg(not(windows))]
Expand Down Expand Up @@ -117,6 +119,10 @@ impl HotReloaderCtx for VirtualMachine {
VirtualMachine::bust_dir_cache(self, path)
}

fn bust_dir_cache_tree(&mut self, path: &[u8]) -> bool {
self.transpiler.resolver.bust_dir_cache_tree(path)
}

fn get_loaders(&self) -> &bun_ast::LoaderHashTable {
&self.transpiler.options.loaders
}
Expand Down Expand Up @@ -210,6 +216,9 @@ pub trait HotReloaderCtx {
/// Returns whether anything was busted.
fn bust_dir_cache(&mut self, path: &[u8]) -> bool;

/// `bust_dir_cache` for `path` and every cached directory below it.
fn bust_dir_cache_tree(&mut self, path: &[u8]) -> bool;

/// `&transpiler.options.loaders`.
fn get_loaders(&self) -> &bun_ast::LoaderHashTable;

Expand Down Expand Up @@ -786,6 +795,83 @@ where
self.tombstones.get(key).copied()
}

/// Writes `dir/name` into `buf`, NUL-terminated. `None` when it does not
/// fit.
Comment thread
robobun marked this conversation as resolved.
Resolved
#[cfg(not(windows))]
fn join_entry_path<'b>(buf: &'b mut PathBuffer, dir: &[u8], name: &[u8]) -> Option<&'b ZStr> {
let dir = strings::trim_right(dir, &[SEP]);
let len = dir.len() + 1 + name.len();
if len >= buf.len() {
return None;
}
buf[..dir.len()].copy_from_slice(dir);
buf[dir.len()] = SEP;
buf[dir.len() + 1..len].copy_from_slice(name);
buf[len] = 0;
// SAFETY: buf[len] == 0 written above.
Some(ZStr::from_buf(&buf[..], len))
}

/// Drops what the resolver cached under `dir/name` and below. Returns
/// true when there was something to drop, which means a resolution went
/// through `dir/name` as a directory.
Comment thread
robobun marked this conversation as resolved.
Resolved
#[cfg(not(windows))]
fn bust_changed_entry(
&mut self,
rfs: &mut Fs::file_system::RealFS,
dir: &[u8],
name: &[u8],
) -> bool {
let mut buf = bun_paths::path_buffer_pool::get();
let Some(child) = Self::join_entry_path(&mut buf, dir, name) else {
return false;
};
self.tombstone_entries(rfs, child.as_bytes());
self.ctx_mut().bust_dir_cache_tree(child.as_bytes())
}

/// Whether the symlink `entry` inside `dir` now resolves somewhere else
/// than where the resolver followed it. Drops the resolver cache for the
/// link when it does.
Comment thread
robobun marked this conversation as resolved.
Resolved
#[cfg(not(windows))]
fn symlink_entry_changed(
&mut self,
rfs: &mut Fs::file_system::RealFS,
dir: &[u8],
entry: &Fs::Entry,
) -> bool {
let mut buf = bun_paths::path_buffer_pool::get();
let Some(child) = Self::join_entry_path(&mut buf, dir, entry.base()) else {
return false;
};
let mut real_buf = bun_paths::path_buffer_pool::get();
if bun_sys::realpath(child, &mut real_buf)
.is_ok_and(|real| real == entry.followed_symlink())
{
return false;
}
self.tombstone_entries(rfs, child.as_bytes());
self.ctx_mut().bust_dir_cache_tree(child.as_bytes());
true
}

/// Keeps the listing of `dir` reachable for later events on it after
/// the resolver cache is busted.
Comment thread
robobun marked this conversation as resolved.
Resolved
#[cfg(not(windows))]
fn tombstone_entries(&mut self, rfs: &mut Fs::file_system::RealFS, dir: &[u8]) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
let Some(existing) = rfs.entries.get(dir) else {
return;
};
let existing: *mut Fs::EntriesOption = existing;
let mut key = bun_paths::path_buffer_pool::get();
if dir.len() + 1 >= key.len() {
return;
}
key[..dir.len()].copy_from_slice(dir);
key[dir.len()] = SEP;
self.put_tombstone(&key[..dir.len() + 1], existing);
}

pub(crate) fn on_error(_: &mut Self, err: &bun_sys::Error) {
// `bun_sys::Error::name()` does the errno→tag-name lookup.
Output::err(err.name(), "Watcher crashed", ());
Expand Down Expand Up @@ -1118,6 +1204,74 @@ where
}
}

// A changed entry the module graph resolved through
// (a retargeted symlink, a replaced directory) has a
// stale real path in the resolver cache.
Comment thread
robobun marked this conversation as resolved.
Resolved
if IS_KQUEUE {
// kqueue names no entry: check every followed symlink.
if let Some(dir_ent) = entries_option {
// SAFETY: dir_ent points into rfs.entries (or a
// tombstoned copy); both outlive this loop
// iteration. Shared access only.
let dir_ent = unsafe { &*dir_ent };
// `bust_dir_cache` takes `entries_mutex` too.
let mut followed: Vec<&'static Fs::Entry> = Vec::new();
{
let _entries_lock = rfs.entries_mutex.lock_guard();
if let Fs::EntriesOption::Entries(listing) = dir_ent {
for &entry_ptr in listing.data.values() {
// SAFETY: EntryStore-owned slot, never freed.
let entry: &'static Fs::Entry = unsafe { &*entry_ptr };
if !entry.followed_symlink().is_empty() {
bun_core::handle_oom(followed.try_reserve(1));
followed.push(entry);
}
}
}
}
for entry in followed {
if self.symlink_entry_changed(rfs, file_path, entry) {
current_task.append(current_hash);
}
}
}
} else {
for changed_name_ in affected_inotify {
let changed_name: &[u8] = match changed_name_ {
Some(z) => z.as_bytes(),
None => continue,
};
if changed_name.is_empty()
|| changed_name[0] == b'~'
|| changed_name[0] == b'.'
{
continue;
}
let followed_symlink = match entries_option {
Some(dir_ent) => {
let _entries_lock = rfs.entries_mutex.lock_guard();
// SAFETY: dir_ent points into rfs.entries
// (or a tombstoned copy); both outlive
// this loop iteration. Shared access only.
match unsafe { &*dir_ent } {
Fs::EntriesOption::Entries(listing) => {
listing.get(changed_name).is_some_and(|e| {
!e.entry().followed_symlink().is_empty()
})
}
Fs::EntriesOption::Err(_) => false,
}
}
None => false,
};
if self.bust_changed_entry(rfs, file_path, changed_name)
Comment thread
robobun marked this conversation as resolved.
Resolved
|| followed_symlink
{
current_task.append(current_hash);
}
}
Comment thread
robobun marked this conversation as resolved.
Resolved
}

if let Some(dir_ent) = entries_option {
// SAFETY: dir_ent points into rfs.entries (or a tombstoned copy);
// both outlive this loop iteration. Shared access only —
Expand Down Expand Up @@ -1342,6 +1496,10 @@ impl<'a> HotReloaderCtx for bun_bundler::BundleV2<'a> {
bun_bundler::BundleV2::bust_dir_cache(self, path)
}

fn bust_dir_cache_tree(&mut self, path: &[u8]) -> bool {
self.transpiler.resolver.bust_dir_cache_tree(path)
}

fn get_loaders(&self) -> &bun_ast::LoaderHashTable {
&self.transpiler.options.loaders
}
Expand Down
8 changes: 8 additions & 0 deletions src/resolver/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ impl Entry {
self.cache.set(c);
}

/// The real path a resolution followed this symlink to. Empty when the
/// entry is not a symlink or was never stat'd. No I/O. Safe from any
/// thread: every `cache` rewrite holds `mutex`.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub fn followed_symlink(&self) -> &'static [u8] {
let _guard = self.mutex.lock_guard();
self.cache().symlink.as_bytes()
}
Comment thread
robobun marked this conversation as resolved.
Resolved

#[inline(always)]
pub(crate) fn set_cache_symlink(&self, symlink: Interned) {
let mut c = self.cache.get();
Expand Down
22 changes: 22 additions & 0 deletions src/resolver/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,12 @@ pub mod fs {
pub(crate) fn remove(&mut self, key: &[u8]) -> bool {
self.inner().remove(key)
}
pub(crate) fn remove_where(&mut self, pred: impl FnMut(&EntriesOption) -> bool) -> usize {
self.inner().remove_where(pred)
}
pub(crate) fn clear_not_found(&mut self) -> usize {
self.inner().clear_not_found()
}
}

/// The active filesystem backend (always the real filesystem).
Expand Down Expand Up @@ -1358,6 +1364,22 @@ pub mod fs {
self.entries.remove(file_path)
}

/// Evicts every cached listing of a directory below `dir_with_slash`
/// (`dir_with_slash` itself excluded). Returns how many were removed.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub(crate) fn bust_entries_cache_below(&mut self, dir_with_slash: &[u8]) -> usize {
let _g = self.entries_mutex.lock_guard();
let listings = self.entries.remove_where(|entry| match entry {
EntriesOption::Entries(listing) => {
listing.dir.len() > dir_with_slash.len()
&& listing.dir.starts_with(dir_with_slash)
}
EntriesOption::Err(_) => false,
});
// A not-found marker has no key to match, and a path that did
// not exist under the old target can exist under the new one.
Comment thread
robobun marked this conversation as resolved.
Resolved
listings + self.entries.clear_not_found()
}

/// lstat + (if symlink) open + fstat +
/// readlink to populate an `EntryCache`. Windows: `GetFileAttributesW` +
/// (if reparse point) `CreateFileW`-follow + `GetFinalPathNameByHandle`
Expand Down
Loading
Loading