Skip to content

Commit fbff79b

Browse files
committed
fix: clear FD_CLOEXEC on stdio fds in spawn_inherited
libuv (used by Node.js) marks stdin/stdout/stderr as close-on-exec, which causes them to be closed when the child process calls exec(). This resulted in the child's fds 0-2 being closed after exec, and Node.js reopening them as /dev/null, silently discarding all output. Add a pre_exec hook to spawn_inherited() that clears FD_CLOEXEC on fds 0, 1, 2 before exec, matching the existing fix in vite-plus's vite_command::fix_stdio_streams(). Ref: libuv/libuv#2062
1 parent 261c567 commit fbff79b

4 files changed

Lines changed: 58 additions & 1 deletion

File tree

crates/vite_task/src/session/execute/mod.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,40 @@ async fn spawn_inherited(spawn_command: &SpawnCommand) -> anyhow::Result<SpawnRe
379379
cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit()).stderr(Stdio::inherit());
380380

381381
let start = std::time::Instant::now();
382-
let mut child = cmd.into_tokio_command().spawn()?;
382+
let mut tokio_cmd = cmd.into_tokio_command();
383+
384+
// Clear FD_CLOEXEC on stdio fds before exec. libuv (used by Node.js) marks
385+
// stdin/stdout/stderr as close-on-exec, which causes them to be closed when
386+
// the child process calls exec(). Without this fix, the child's fds 0-2 are
387+
// closed after exec and Node.js reopens them as /dev/null, losing all output.
388+
// See: https://github.com/libuv/libuv/issues/2062
389+
// SAFETY: The pre_exec closure only performs fcntl operations to clear
390+
// FD_CLOEXEC flags on stdio fds, which is safe in a post-fork context.
391+
#[cfg(unix)]
392+
unsafe {
393+
tokio_cmd.pre_exec(|| {
394+
use std::os::fd::BorrowedFd;
395+
396+
use nix::{
397+
fcntl::{FcntlArg, FdFlag, fcntl},
398+
libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
399+
};
400+
for fd in [STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO] {
401+
// SAFETY: fds 0-2 are always valid in a post-fork context
402+
let borrowed = BorrowedFd::borrow_raw(fd);
403+
if let Ok(flags) = fcntl(borrowed, FcntlArg::F_GETFD) {
404+
let mut fd_flags = FdFlag::from_bits_retain(flags);
405+
if fd_flags.contains(FdFlag::FD_CLOEXEC) {
406+
fd_flags.remove(FdFlag::FD_CLOEXEC);
407+
let _ = fcntl(borrowed, FcntlArg::F_SETFD(fd_flags));
408+
}
409+
}
410+
}
411+
Ok(())
412+
});
413+
}
414+
415+
let mut child = tokio_cmd.spawn()?;
383416
let exit_status = child.wait().await?;
384417

385418
Ok(SpawnResult { exit_status, duration: start.elapsed() })
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "script-stdout-inherited-test",
3+
"scripts": {
4+
"foo": "node -e \"console.log(5173)\""
5+
}
6+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Tests that stdout from a package.json script is captured correctly
2+
# when stdio is inherited (single task, no caching).
3+
#
4+
# Reproduces a bug where `node -e "console.log(5173)"` output was
5+
# lost when executed via `vp run` in non-PTY environments.
6+
7+
[[e2e]]
8+
name = "script stdout captured with inherited stdio"
9+
steps = [
10+
"vp run foo",
11+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
3+
expression: e2e_outputs
4+
---
5+
> vp run foo
6+
$ node -e "console.log(5173)"cache disabled
7+
5173

0 commit comments

Comments
 (0)