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
58 changes: 58 additions & 0 deletions src/runtime/webcore/blob/read_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@ pub struct ReadFile {
pub(crate) io_request: io::Request,
#[cfg(not(windows))]
pub(crate) could_block: bool,
/// FIFO vnode (not a `pipe(2)` pipe); see `bun_sys::block_until_readable`.
#[cfg(target_os = "macos")]
pub(crate) is_named_pipe: bool,
pub(crate) close_after_io: bool,
pub(crate) state: AtomicU8, // ClosingState
}
Expand Down Expand Up @@ -380,6 +383,8 @@ impl ReadFile {
scheduled: false,
},
could_block: false,
#[cfg(target_os = "macos")]
is_named_pipe: false,
close_after_io: false,
state: AtomicU8::new(ClosingState::Running as u8),
};
Expand Down Expand Up @@ -465,6 +470,37 @@ impl ReadFile {
}
}

/// `wait_for_readable` for named pipes, on this thread; `false` if the wait itself failed.
#[cfg(target_os = "macos")]
fn block_until_readable(&mut self) -> bool {
bloblog!("ReadFile.blockUntilReadable");
match bun_sys::block_until_readable(self.opened_fd) {
Ok(()) => true,
Err(err) => {
self.errno = Some(bun_errno::from_errno(err.errno as i32).into());
self.system_error = Some(err.to_system_error().into());
false
}
}
}

/// A named pipe's whole read, as its own pool task: `JobContext::run` holds a
/// VM borrow and VM teardown waits for borrows, so waiting for a writer must
/// not happen inside it. Waiting before the first read also keeps a FIFO
/// with no writer yet from reading as empty.
Comment thread
robobun marked this conversation as resolved.
#[cfg(target_os = "macos")]
fn read_named_pipe_task(task: *mut WorkPoolTask) {
// SAFETY: only reached via `WorkPoolTask::callback` with `task` =
// `&mut self.task` (intrusive) scheduled by `run_async_with_fd`;
// recover parent.
let this = unsafe { &mut *ReadFile::from_task_ptr(task) };
if this.block_until_readable() {
this.do_read_loop();
} else {
this.on_finish();
}
}

/// Pick the read target: `buffer`'s spare capacity if it is at least as
/// large as `stack_buffer`, otherwise `stack_buffer`; capped by
/// `max_length - read_off`. Returns `(use_stack, target)` so the caller
Expand Down Expand Up @@ -682,6 +718,11 @@ impl ReadFile {
}

self.could_block = !bun_sys::is_regular_file(stat.st_mode as _);
#[cfg(target_os = "macos")]
{
// pipe(2) pipes are S_IFIFO with st_dev == 0 (XNU pipe_stat).
self.is_named_pipe = bun_sys::S::ISFIFO(stat.st_mode as _) && stat.st_dev != 0;
}
self.total_size =
SizeType::try_from((stat.st_size as i64).max(0).min(MAX_SIZE as i64)).unwrap();

Expand Down Expand Up @@ -754,6 +795,16 @@ impl ReadFile {
// If we immediately call read(), it will block until stdin is
// readable.
if self.could_block {
#[cfg(target_os = "macos")]
if self.is_named_pipe {
self.task = WorkPoolTask {
node: Default::default(),
callback: Self::read_named_pipe_task,
};
WorkPool::schedule(&raw mut self.task);
return;
}

if bun_core::is_readable(fd) == bun_core::Pollable::NotReady {
self.wait_for_readable();
return;
Expand Down Expand Up @@ -855,6 +906,13 @@ impl ReadFile {
// call. We already know it's done.
&& !self.read_eof)
{
#[cfg(target_os = "macos")]
if self.is_named_pipe {
if self.block_until_readable() {
continue;
}
break;
}
if self.could_block
// If we received EOF, we can skip the poll() system
// call. We already know it's done.
Expand Down
49 changes: 48 additions & 1 deletion src/sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1411,14 +1411,16 @@ impl Tag {
#[cfg(not(windows))]
pub(crate) const setrlimit: Tag = Tag(106);
pub const clone3: Tag = Tag(107);
#[cfg(target_os = "macos")]
pub(crate) const select: Tag = Tag(108);
// `inotify_init1`/`inotify_add_watch` fold under the generic `.watch`
// tag; `INotifyWatcher.rs` spells it `.inotify`. Alias to `.watch`
// so the JS-facing `err.syscall == "watch"` string stays node-compatible.

/// The tag name — spelling is frozen (JS-facing
/// `err.syscall` string; node-compat code matches on it).
pub fn name(self) -> &'static str {
const NAMES: [&str; 108] = [
const NAMES: [&str; 109] = [
"TODO",
"dup",
"access",
Expand Down Expand Up @@ -1528,6 +1530,7 @@ impl Tag {
"getrlimit",
"setrlimit",
"clone3",
"select",
];
NAMES.get(self.0 as usize).copied().unwrap_or("unknown")
}
Expand Down Expand Up @@ -1708,6 +1711,15 @@ mod nocancel {
) -> isize;
#[link_name = "poll$NOCANCEL"]
pub(crate) fn poll(fds: *mut libc::pollfd, nfds: libc::nfds_t, timeout: c_int) -> c_int;
// `_DARWIN_UNLIMITED_SELECT` select(2): no FD_SETSIZE limit; a set is ceil(nfds/32) words.
#[link_name = "select$DARWIN_EXTSN$NOCANCEL"]
pub(crate) fn select(
nfds: c_int,
readfds: *mut u32,
writefds: *mut u32,
errorfds: *mut u32,
timeout: *mut libc::timeval,
) -> c_int;
// Remaining `$NOCANCEL` variants Bun links against.
// safe: by-value `c_int` fd; bad fd → -1/EBADF, no UB.
#[link_name = "close$NOCANCEL"]
Expand Down Expand Up @@ -7607,6 +7619,41 @@ pub fn kevent(
}
}

/// Blocks in `select(2)` until `fd` is readable or at EOF; retries on EINTR.
///
/// For named pipes: XNU's kqueue filter for a FIFO (`filt_vnode_common`, which
/// `poll(2)` uses too) fires only while bytes are buffered, never for the last
/// writer closing; `fifo_select` consults the FIFO's socket, which reports that
/// close (and not a writer that has yet to connect). `pipe(2)` pipes report `EV_EOF`.
Comment thread
robobun marked this conversation as resolved.
#[cfg(target_os = "macos")]
pub fn block_until_readable(fd: Fd) -> Maybe<()> {
debug_assert!(fd.is_valid());
let index = fd.native() as usize;
let word = index / 32;
let mut read_set = vec![0u32; word + 1];
loop {
read_set.fill(0);
read_set[word] = 1 << (index % 32);
// SAFETY: `read_set` holds the ceil(nfds / 32) words the kernel reads
// and writes back for `nfds = fd + 1`; the write and error sets and the
// timeout (wait indefinitely) may be null.
let rc = unsafe {
nocancel::select(
fd.native() + 1,
read_set.as_mut_ptr(),
core::ptr::null_mut(),
core::ptr::null_mut(),
core::ptr::null_mut(),
)
};
match get_errno(rc) {
E::SUCCESS => return Ok(()),
E::EINTR => continue,
e => return Err(Error::from_code(e, Tag::select).with_fd(fd)),
}
}
}

/// `clonefile` — macOS-only CoW copy. On non-Darwin returns ENOTSUP so
/// callers can fall back to `copy_file`.
#[cfg(not(target_os = "macos"))]
Expand Down
Loading