Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
34 changes: 29 additions & 5 deletions src/runtime/webcore/blob/copy_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,6 @@ impl CopyFile {
}

loop {
// TODO: this should use non-blocking I/O.
let written: isize = match USE {
TryWith::CopyFileRange => {
// SAFETY: raw copy_file_range(2); both fds owned by caller, null offsets.
Expand Down Expand Up @@ -427,6 +426,20 @@ impl CopyFile {
match bun_sys::get_errno(written) {
bun_sys::E::SUCCESS => {}

// One of the fds is O_NONBLOCK (`Bun.stdout` is, once
// `process.stdout` has been touched). A pipe-to-pipe splice is
// non-blocking as a whole if either end is, so EAGAIN does not
// say which side would have blocked: wait for both, then retry.
Comment thread
robobun marked this conversation as resolved.
Outdated
bun_sys::E::EAGAIN => {
if let Err(err) = bun_sys::block_until_readable(src_fd)
.and_then(|()| bun_sys::block_until_writable(dest_fd))
{
self.system_error = Some(err.to_system_error());
return Err(bun_errno::from_errno(err.errno as i32).into());
}
continue;
}

// XDEV: cross-device copy not supported
// NOSYS: syscall not available
// OPNOTSUPP: filesystem doesn't support this operation
Expand Down Expand Up @@ -992,19 +1005,30 @@ fn read_write_loop_capped(
let mut remaining = cap;
while remaining > 0 {
let want = (buf.len() as SizeType).min(remaining) as usize;
let amt = bun_sys::read(src_fd, &mut buf[..want])?;
// Either fd may be O_NONBLOCK (`Bun.stdout` is once `process.stdout` has
// been touched); wait for it and retry instead of failing the copy.
Comment thread
robobun marked this conversation as resolved.
Outdated
let amt = match bun_sys::read(src_fd, &mut buf[..want]) {
Ok(amt) => amt,
Err(err) if err.is_retry() => {
bun_sys::block_until_readable(src_fd)?;
continue;
}
Err(err) => return Err(err),
};
if amt == 0 {
break;
}
remaining -= amt as SizeType;
let mut slice = &buf[..amt];
while !slice.is_empty() {
match bun_sys::write(dest_fd, slice)? {
0 => return Ok(()),
n => {
match bun_sys::write(dest_fd, slice) {
Ok(0) => return Ok(()),
Ok(n) => {
*total += n as u64;
slice = &slice[n..];
}
Err(err) if err.is_retry() => bun_sys::block_until_writable(dest_fd)?,
Err(err) => return Err(err),
}
}
}
Expand Down
110 changes: 109 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,16 @@ 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) 32-bit words rather than a `libc::fd_set`.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[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 @@ -7630,6 +7643,101 @@ pub fn kevent(
}
}

// ── block_until_readable / block_until_writable ──
//
// For loops that copy blocking-style on a pool thread but may be handed an fd
// whose open file description is in O_NONBLOCK mode (`process.stdout` puts the
// inherited stdio descriptions there, and the flag travels with a description
// into child processes). On EAGAIN the loop waits here and retries, which is
// what a blocking description would have done inside the syscall. Both return
// as soon as the retried syscall will not block again, including when what it
// is going to report is EOF or an error such as EPIPE: the retry reports it.
Comment thread
robobun marked this conversation as resolved.
Outdated

/// Blocks until a `read`-side syscall on `fd` would make progress. EINTR is retried.
#[cfg(all(unix, not(target_os = "macos")))]
pub fn block_until_readable(fd: Fd) -> Maybe<()> {
block_until(fd, posix::POLL_IN)
}

/// Blocks until a `write`-side syscall on `fd` would make progress. EINTR is retried.
#[cfg(all(unix, not(target_os = "macos")))]
pub fn block_until_writable(fd: Fd) -> Maybe<()> {
block_until(fd, posix::POLL_OUT)
}

#[cfg(all(unix, not(target_os = "macos")))]
fn block_until(fd: Fd, events: i16) -> Maybe<()> {
debug_assert!(fd.is_valid());
let mut fds = [posix::PollFd {
fd: fd.native(),
events,
revents: 0,
}];
// POLLHUP / POLLERR / POLLNVAL are reported without being requested, so
// any return means the caller's retry settles the matter.
Comment thread
robobun marked this conversation as resolved.
Outdated
match posix::poll(&mut fds, -1) {
Ok(_) => Ok(()),
Err(err) => Err(err.with_fd(fd)),
}
}

/// Blocks until a `read`-side syscall on `fd` would make progress. EINTR is retried.
#[cfg(target_os = "macos")]
pub fn block_until_readable(fd: Fd) -> Maybe<()> {
select_one(fd, SelectFor::Read)
}

/// Blocks until a `write`-side syscall on `fd` would make progress. EINTR is retried.
#[cfg(target_os = "macos")]
pub fn block_until_writable(fd: Fd) -> Maybe<()> {
select_one(fd, SelectFor::Write)
}

#[cfg(target_os = "macos")]
#[derive(Clone, Copy)]
enum SelectFor {
Read,
Write,
}

/// `select(2)` rather than `poll(2)`: XNU implements poll on the kqueue vnode
/// filter, which for a named pipe (FIFO) fires only while bytes (or space) are
/// available and never for the other end closing, so a poll would outlive the
/// peer. `fifo_select` consults the FIFO's socket, which does report the close.
/// `pipe(2)` pipes, sockets and ttys behave under select exactly as under poll.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[cfg(target_os = "macos")]
fn select_one(fd: Fd, what: SelectFor) -> Maybe<()> {
debug_assert!(fd.is_valid());
let index = fd.native() as usize;
let word = index / 32;
let mut set = vec![0u32; word + 1];
loop {
set.fill(0);
set[word] = 1 << (index % 32);
let (read_set, write_set) = match what {
SelectFor::Read => (set.as_mut_ptr(), core::ptr::null_mut()),
SelectFor::Write => (core::ptr::null_mut(), set.as_mut_ptr()),
};
// SAFETY: `set` holds the ceil(nfds / 32) words the kernel reads and
// writes back for `nfds = fd + 1`; the other sets and the timeout
// (wait indefinitely) may be null.
let rc = unsafe {
nocancel::select(
fd.native() + 1,
read_set,
write_set,
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
Loading