diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/check_backpressure/snapshots/check_backpressure_nonblocking_stdout.global.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/check_backpressure/snapshots/check_backpressure_nonblocking_stdout.global.md index f233a35978..6565962928 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/check_backpressure/snapshots/check_backpressure_nonblocking_stdout.global.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/check_backpressure/snapshots/check_backpressure_nonblocking_stdout.global.md @@ -4,8 +4,25 @@ vp check exposes the stdout EAGAIN failure when a large diagnostic replay meets ## `vpt backpressure-run --digest 6,8 -- vp check` -**Exit code:** 1 - ``` -backpressure-run detected truncated child output under stdio backpressure +--- stdout --- +stdout: 1282 lines +pass: All 3 files are correctly formatted (, threads) +! eslint(no-unused-vars): Variable 'unused000' is declared but never used. Unused variables should start with a '_'. + ,-[src/index.js:2:9] + 1 | export function emitDiagnostics() { + 2 | const unused000 = 0; + : ^^^^|^^^^ +... 1268 lines elided ... + 129 | const unused127 = 127; + : ^^^^|^^^^ + : `-- 'unused127' is declared here + 130 | } + `---- + help: Consider removing this declaration. + +Found 0 errors and 128 warnings in 2 files (, threads) +--- stderr --- +stderr: 1 lines +warn: Lint warnings found ``` diff --git a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/check_backpressure/snapshots/check_backpressure_nonblocking_stdout.local.md b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/check_backpressure/snapshots/check_backpressure_nonblocking_stdout.local.md index f233a35978..6565962928 100644 --- a/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/check_backpressure/snapshots/check_backpressure_nonblocking_stdout.local.md +++ b/crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/check_backpressure/snapshots/check_backpressure_nonblocking_stdout.local.md @@ -4,8 +4,25 @@ vp check exposes the stdout EAGAIN failure when a large diagnostic replay meets ## `vpt backpressure-run --digest 6,8 -- vp check` -**Exit code:** 1 - ``` -backpressure-run detected truncated child output under stdio backpressure +--- stdout --- +stdout: 1282 lines +pass: All 3 files are correctly formatted (, threads) +! eslint(no-unused-vars): Variable 'unused000' is declared but never used. Unused variables should start with a '_'. + ,-[src/index.js:2:9] + 1 | export function emitDiagnostics() { + 2 | const unused000 = 0; + : ^^^^|^^^^ +... 1268 lines elided ... + 129 | const unused127 = 127; + : ^^^^|^^^^ + : `-- 'unused127' is declared here + 130 | } + `---- + help: Consider removing this declaration. + +Found 0 errors and 128 warnings in 2 files (, threads) +--- stderr --- +stderr: 1 lines +warn: Lint warnings found ``` diff --git a/crates/vite_global_cli/src/main.rs b/crates/vite_global_cli/src/main.rs index 1fdd658238..5a90eef107 100644 --- a/crates/vite_global_cli/src/main.rs +++ b/crates/vite_global_cli/src/main.rs @@ -296,6 +296,8 @@ fn print_unknown_argument_error(error: &clap::Error) -> bool { #[tokio::main] async fn main() -> ExitCode { + vite_shared::ensure_blocking_stdio(); + // Initialize tracing vite_shared::init_tracing(); diff --git a/crates/vite_shared/Cargo.toml b/crates/vite_shared/Cargo.toml index 62dbde59a6..b40385dc27 100644 --- a/crates/vite_shared/Cargo.toml +++ b/crates/vite_shared/Cargo.toml @@ -9,7 +9,7 @@ rust-version.workspace = true [dependencies] directories = { workspace = true } -nix = { workspace = true, features = ["poll", "term"] } +nix = { workspace = true, features = ["fs", "poll", "term"] } owo-colors = { workspace = true } serde = { workspace = true } # use `preserve_order` feature to preserve the order of the fields in `package.json` diff --git a/crates/vite_shared/src/lib.rs b/crates/vite_shared/src/lib.rs index 5abf7b727f..d8d7d7ea2d 100644 --- a/crates/vite_shared/src/lib.rs +++ b/crates/vite_shared/src/lib.rs @@ -17,6 +17,7 @@ mod json_edit; pub mod output; mod package_json; mod path_env; +mod stdio; pub mod string_similarity; mod tls; mod tracing; @@ -33,5 +34,6 @@ pub use path_env::{ PrependOptions, PrependResult, format_path_prepended, format_path_with_prepend, prepend_to_path_env, }; +pub use stdio::ensure_blocking_stdio; pub use tls::ensure_tls_provider; pub use tracing::init_tracing; diff --git a/crates/vite_shared/src/stdio.rs b/crates/vite_shared/src/stdio.rs new file mode 100644 index 0000000000..16f007703d --- /dev/null +++ b/crates/vite_shared/src/stdio.rs @@ -0,0 +1,62 @@ +/// Ensure inherited standard streams use the blocking semantics expected by +/// Rust's standard library and by the tools embedded in Vite+. +/// +/// Node.js marks non-TTY stdio as non-blocking, and child processes inherit +/// that flag through the shared open file description. Clear `O_NONBLOCK` +/// once at process entry so unowned writers do not panic or truncate output +/// when a consumer applies backpressure. +#[cfg(unix)] +pub fn ensure_blocking_stdio() { + use std::{io, os::fd::AsFd}; + + let stdin = io::stdin(); + let stdout = io::stdout(); + let stderr = io::stderr(); + + for fd in [stdin.as_fd(), stdout.as_fd(), stderr.as_fd()] { + ensure_blocking_fd(fd); + } +} + +#[cfg(unix)] +fn ensure_blocking_fd(fd: std::os::fd::BorrowedFd<'_>) { + use nix::{ + fcntl::{FcntlArg, OFlag, fcntl}, + unistd::isatty, + }; + + if isatty(fd).unwrap_or(false) { + return; + } + + let Ok(flags) = fcntl(fd, FcntlArg::F_GETFL) else { + return; + }; + let flags = OFlag::from_bits_retain(flags); + if flags.contains(OFlag::O_NONBLOCK) { + let _ = fcntl(fd, FcntlArg::F_SETFL(flags.difference(OFlag::O_NONBLOCK))); + } +} + +#[cfg(not(unix))] +pub fn ensure_blocking_stdio() {} + +#[cfg(all(test, unix))] +mod tests { + use std::os::{fd::AsFd, unix::net::UnixStream}; + + use nix::fcntl::{FcntlArg, OFlag, fcntl}; + + use super::ensure_blocking_fd; + + #[test] + fn clears_nonblocking_from_a_non_tty_file_description() { + let (stream, _peer) = UnixStream::pair().unwrap(); + stream.set_nonblocking(true).unwrap(); + + ensure_blocking_fd(stream.as_fd()); + + let flags = fcntl(&stream, FcntlArg::F_GETFL).unwrap(); + assert!(!OFlag::from_bits_retain(flags).contains(OFlag::O_NONBLOCK)); + } +} diff --git a/packages/cli/binding/src/lib.rs b/packages/cli/binding/src/lib.rs index a310a88b50..4463731e8c 100644 --- a/packages/cli/binding/src/lib.rs +++ b/packages/cli/binding/src/lib.rs @@ -42,6 +42,7 @@ use crate::cli::{ #[napi_derive::module_init] #[allow(clippy::disallowed_macros)] pub fn init() { + vite_shared::ensure_blocking_stdio(); crate::cli::init_tracing(); // Install a Vite+ panic hook so panics are correctly attributed to Vite+.