Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions src/event_loop/MiniEventLoop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,8 @@ bun_io::link_impl_EventLoopCtx! {
(*this).after_event_loop_callback_ctx = ctx;
},
pipe_read_scratch() => &raw const *(*this).pipe_read_scratch,
// No raw mode on the mini loop: a reader of fd 0 opens its own tty.
stdin_tty() => core::ptr::null_mut(),
}
}

Expand Down
48 changes: 27 additions & 21 deletions src/io/PipeReader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1236,7 +1236,7 @@ impl WindowsBufferedReader {
let size = limit.clamp_len(suggested_size);
// Tty reads must not target `_buffer`: libuv can retain the pointer
// past reader teardown (see `uv::Tty::read_scratch` for the contract).
if matches!(self.source, Some(Source::Tty(_))) {
if matches!(self.source, Some(Source::Tty(_) | Source::StdinTty(_))) {
let scratch = self
.source
.as_mut()
Expand Down Expand Up @@ -1311,7 +1311,7 @@ impl WindowsBufferedReader {
// Use the event loop from the parent, not the global one
// This is critical for spawnSync to use its isolated loop
let loop_ = self.vtable.loop_();
let source = match Source::open(loop_.cast(), fd) {
let source = match Source::open(loop_.cast(), fd, Some(self.vtable.event_loop())) {
sys::Result::Err(err) => return sys::Result::Err(err),
sys::Result::Ok(source) => source,
};
Expand Down Expand Up @@ -1408,7 +1408,7 @@ impl WindowsBufferedReader {
let mut b = unsafe { *buf };
let slice = unsafe { b.slice_mut() };
let data = &mut slice[..len];
if matches!(this.source, Some(Source::Tty(_))) {
if matches!(this.source, Some(Source::Tty(_) | Source::StdinTty(_))) {
// Tty chunks arrive in the tty-owned scratch; stage them
// into `_buffer` so `on_read` commits them like a pipe chunk.
this._buffer.reserve(len);
Expand Down Expand Up @@ -1668,7 +1668,7 @@ impl WindowsBufferedReader {
return sys::Result::Err(err);
}
}
Source::Pipe(_) | Source::Tty(_) => {
Source::Pipe(_) | Source::Tty(_) | Source::StdinTty(_) => {
// SAFETY: source is a live Pipe/Tty stream handle.
if let Some(err) = unsafe {
uv::uv_read_start(
Expand Down Expand Up @@ -1707,7 +1707,7 @@ impl WindowsBufferedReader {
Source::File(file) | Source::SyncFile(file) => {
file.stop();
}
Source::Pipe(_) | Source::Tty(_) => {
Source::Pipe(_) | Source::Tty(_) | Source::StdinTty(_) => {
// SAFETY: stream handle is live (just matched a stream source).
unsafe { uv::uv_read_stop(source.to_stream()) };
}
Expand Down Expand Up @@ -1759,18 +1759,26 @@ impl WindowsBufferedReader {
#[cfg(windows)]
Source::Tty(tty) => {
let p = tty.as_ptr();
if crate::source::stdin_tty::is_stdin_tty(p) {
// Node only ever closes stdin on process exit.
} else {
// SAFETY: tty is a live heap-allocated Tty*;
// `Tty::close` keeps whole-struct provenance so
// on_tty_close may reclaim the Box.
unsafe {
(*p).uv.data = p.cast::<c_void>();
crate::source::Tty::close(p, Self::on_tty_close);
// SAFETY: tty is a live heap-allocated Tty*;
// `Tty::close` keeps whole-struct provenance so
// on_tty_close may reclaim the Box.
unsafe {
(*p).uv.data = p.cast::<c_void>();
crate::source::Tty::close(p, Self::on_tty_close);
}
self.flags.insert(WindowsFlags::IS_PAUSED);
}
#[cfg(windows)]
Source::StdinTty(tty) => {
// Stays open for the VM's next stdin reader.
let p = tty.as_ptr();
// SAFETY: the VM's shared tty outlives this reader.
unsafe {
if (*p).uv.data == core::ptr::from_mut(self).cast::<c_void>() {
uv::uv_read_stop(p.cast());
(*p).uv.data = core::ptr::null_mut();
}
}

self.flags.insert(WindowsFlags::IS_PAUSED);
}
#[cfg(not(windows))]
Expand Down Expand Up @@ -1845,12 +1853,10 @@ impl WindowsBufferedReader {
extern "C" fn on_tty_close(handle: *mut uv::uv_tty_t) {
// `close_impl` set `handle.data = handle` and called `uv_close(handle)`;
// libuv passes the same pointer back; `Tty::from_uv` recovers the
// owning `Tty`. Caller gates on `!is_stdin_tty`, so it is heap-owned,
// and no request is pending once this runs (`uv::Tty::read_scratch`).
let tty = crate::source::Tty::from_uv(handle);
debug_assert!(!crate::source::stdin_tty::is_stdin_tty(tty));
// SAFETY: non-stdin tty is heap-allocated; sole owner after uv_close.
drop(unsafe { bun_core::heap::take(tty) });
// owning `Tty`. Only a `Source::Tty` (heap, `open_tty`) is closed this
// way, and no request is pending once this runs (`uv::Tty::read_scratch`).
// SAFETY: heap-owned; sole owner after uv_close.
drop(unsafe { bun_core::heap::take(crate::source::Tty::from_uv(handle)) });
}

fn on_read(&mut self, amount: sys::Result<usize>, slice: &mut [u8], has_more: ReadState) {
Expand Down
16 changes: 7 additions & 9 deletions src/io/PipeWriter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1186,13 +1186,14 @@ pub trait BaseWindowsPipeWriter: Sized {
}
Source::Tty(tty) => {
let p = tty.as_ptr();
// SAFETY: tty is heap-allocated (via open_tty) or the
// process-static stdin tty; freed in on_tty_close (gated on is_stdin_tty).
// SAFETY: tty is heap-allocated (open_tty); freed in on_tty_close.
unsafe { (*p).uv.data = p.cast::<c_void>() };
// SAFETY: tty is a live uv handle; `Tty::close` keeps
// whole-struct provenance so on_tty_close may reclaim the Box.
unsafe { crate::source::Tty::close(p, on_tty_close) };
}
// Not produced for a writer (`start` opens without a reader context).
Source::StdinTty(_) => {}
}
*self.source_mut() = None;
self.on_close_source();
Expand Down Expand Up @@ -1271,7 +1272,7 @@ pub trait BaseWindowsPipeWriter: Sized {
// This is critical for spawnSync to use its isolated loop
// SAFETY: parent is BACKREF set via set_parent; valid while writer alive.
let loop_ = unsafe { Self::Parent::loop_(self.parent_ptr()) };
let mut source = match Source::open(loop_, fd) {
let mut source = match Source::open(loop_, fd, None) {
sys::Result::Ok(source) => source,
sys::Result::Err(err) => return sys::Result::Err(err),
};
Expand Down Expand Up @@ -1324,12 +1325,9 @@ extern "C" fn on_pipe_close(handle: *mut uv::Pipe) {
extern "C" fn on_tty_close(handle: *mut uv::uv_tty_t) {
// `close()` set `handle.data = handle` and then called `uv_close(handle)`;
// libuv passes the same pointer back; `Tty::from_uv` recovers the owning
// `Tty`. The stdin tty (fd 0) lives in static storage; never free it.
let tty = crate::source::Tty::from_uv(handle);
if !crate::source::stdin_tty::is_stdin_tty(tty) {
// SAFETY: non-stdin tty is heap-allocated (open_tty).
drop(unsafe { bun_core::heap::take(tty) });
}
// `Tty`. Only a `Source::Tty` (heap, `open_tty`) is closed this way.
// SAFETY: heap-owned; sole owner after uv_close.
drop(unsafe { bun_core::heap::take(crate::source::Tty::from_uv(handle)) });
}

/// Common parent requirements for Windows writers (event loop access + ref counting).
Expand Down
7 changes: 7 additions & 0 deletions src/io/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,8 @@ bun_dispatch::link_interface! {
ctx: Option<core::ptr::NonNull<core::ffi::c_void>>,
);
fn pipe_read_scratch() -> *const PipeReadScratch;
// Null when the context has none (mini loop, POSIX).
fn stdin_tty() -> *mut StdinTty;
}
}

Expand Down Expand Up @@ -471,6 +473,11 @@ pub use pipe_read_scratch::{PipeReadScratch, PipeReadScratchGuard};
#[cfg(windows)]
#[path = "source.rs"]
pub mod source;
#[cfg(windows)]
pub use source::StdinTty;
/// Never exists on POSIX: [`EventLoopCtx::stdin_tty`] returns null there.
#[cfg(not(windows))]
pub enum StdinTty {}
#[path = "write.rs"]
pub mod write;

Expand Down
Loading
Loading