Skip to content

Commit 31fc7dc

Browse files
committed
fix(cli): ensure blocking stdio
1 parent aaa12f4 commit 31fc7dc

7 files changed

Lines changed: 103 additions & 10 deletions

File tree

crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/check_backpressure/snapshots/check_backpressure_nonblocking_stdout.global.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,25 @@ vp check exposes the stdout EAGAIN failure when a large diagnostic replay meets
44

55
## `vpt backpressure-run --digest 6,8 -- vp check`
66

7-
**Exit code:** 1
8-
97
```
108
--- stdout ---
11-
stdout: 4 lines
9+
stdout: 1282 lines
1210
pass: All 3 files are correctly formatted (<duration>, <n> threads)
11+
! eslint(no-unused-vars): Variable 'unused000' is declared but never used. Unused variables should start with a '_'.
12+
,-[src/index.js:2:9]
13+
1 | export function emitDiagnostics() {
14+
2 | const unused000 = 0;
15+
: ^^^^|^^^^
16+
... 1268 lines elided ...
17+
129 | const unused127 = 127;
18+
: ^^^^|^^^^
19+
: `-- 'unused127' is declared here
20+
130 | }
21+
`----
22+
help: Consider removing this declaration.
1323
1424
Found 0 errors and 128 warnings in 2 files (<duration>, <n> threads)
1525
--- stderr ---
1626
stderr: 1 lines
1727
warn: Lint warnings found
18-
backpressure-run payload was not tense enough: captured 152 bytes, expected at least 4096 (4x the measured channel capacity)
1928
```

crates/vite_cli_snapshots/tests/cli_snapshots/fixtures/check_backpressure/snapshots/check_backpressure_nonblocking_stdout.local.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,25 @@ vp check exposes the stdout EAGAIN failure when a large diagnostic replay meets
44

55
## `vpt backpressure-run --digest 6,8 -- vp check`
66

7-
**Exit code:** 1
8-
97
```
108
--- stdout ---
11-
stdout: 4 lines
9+
stdout: 1282 lines
1210
pass: All 3 files are correctly formatted (<duration>, <n> threads)
11+
! eslint(no-unused-vars): Variable 'unused000' is declared but never used. Unused variables should start with a '_'.
12+
,-[src/index.js:2:9]
13+
1 | export function emitDiagnostics() {
14+
2 | const unused000 = 0;
15+
: ^^^^|^^^^
16+
... 1268 lines elided ...
17+
129 | const unused127 = 127;
18+
: ^^^^|^^^^
19+
: `-- 'unused127' is declared here
20+
130 | }
21+
`----
22+
help: Consider removing this declaration.
1323
1424
Found 0 errors and 128 warnings in 2 files (<duration>, <n> threads)
1525
--- stderr ---
1626
stderr: 1 lines
1727
warn: Lint warnings found
18-
backpressure-run payload was not tense enough: captured 152 bytes, expected at least 4096 (4x the measured channel capacity)
1928
```

crates/vite_global_cli/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ fn print_unknown_argument_error(error: &clap::Error) -> bool {
296296

297297
#[tokio::main]
298298
async fn main() -> ExitCode {
299+
vite_shared::ensure_blocking_stdio();
300+
299301
// Initialize tracing
300302
vite_shared::init_tracing();
301303

crates/vite_shared/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ rust-version.workspace = true
99

1010
[dependencies]
1111
directories = { workspace = true }
12-
nix = { workspace = true, features = ["poll", "term"] }
12+
nix = { workspace = true, features = ["fs", "poll", "term"] }
1313
owo-colors = { workspace = true }
1414
serde = { workspace = true }
1515
# use `preserve_order` feature to preserve the order of the fields in `package.json`

crates/vite_shared/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod json_edit;
1717
pub mod output;
1818
mod package_json;
1919
mod path_env;
20+
mod stdio;
2021
pub mod string_similarity;
2122
mod tls;
2223
mod tracing;
@@ -33,5 +34,6 @@ pub use path_env::{
3334
PrependOptions, PrependResult, format_path_prepended, format_path_with_prepend,
3435
prepend_to_path_env,
3536
};
37+
pub use stdio::ensure_blocking_stdio;
3638
pub use tls::ensure_tls_provider;
3739
pub use tracing::init_tracing;

crates/vite_shared/src/stdio.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/// Ensure inherited standard streams use the blocking semantics expected by
2+
/// Rust's standard library and by the tools embedded in Vite+.
3+
///
4+
/// Node.js marks non-TTY stdio as non-blocking, and child processes inherit
5+
/// that flag through the shared open file description. Clear `O_NONBLOCK`
6+
/// once at process entry so unowned writers do not panic or truncate output
7+
/// when a consumer applies backpressure.
8+
#[cfg(unix)]
9+
pub fn ensure_blocking_stdio() {
10+
use std::{io, os::fd::AsFd};
11+
12+
let stdin = io::stdin();
13+
let stdout = io::stdout();
14+
let stderr = io::stderr();
15+
16+
for fd in [stdin.as_fd(), stdout.as_fd(), stderr.as_fd()] {
17+
ensure_blocking_fd(fd);
18+
}
19+
}
20+
21+
#[cfg(unix)]
22+
fn ensure_blocking_fd(fd: std::os::fd::BorrowedFd<'_>) {
23+
use nix::{
24+
fcntl::{FcntlArg, OFlag, fcntl},
25+
unistd::isatty,
26+
};
27+
28+
if isatty(fd).unwrap_or(false) {
29+
return;
30+
}
31+
32+
let Ok(flags) = fcntl(fd, FcntlArg::F_GETFL) else {
33+
return;
34+
};
35+
let flags = OFlag::from_bits_retain(flags);
36+
if flags.contains(OFlag::O_NONBLOCK) {
37+
let _ = fcntl(fd, FcntlArg::F_SETFL(flags.difference(OFlag::O_NONBLOCK)));
38+
}
39+
}
40+
41+
#[cfg(not(unix))]
42+
pub fn ensure_blocking_stdio() {}
43+
44+
#[cfg(all(test, unix))]
45+
mod tests {
46+
use std::os::{fd::AsFd, unix::net::UnixStream};
47+
48+
use nix::fcntl::{FcntlArg, OFlag, fcntl};
49+
50+
use super::ensure_blocking_fd;
51+
52+
#[test]
53+
fn clears_nonblocking_from_a_non_tty_file_description() {
54+
let (stream, _peer) = UnixStream::pair().unwrap();
55+
stream.set_nonblocking(true).unwrap();
56+
57+
ensure_blocking_fd(stream.as_fd());
58+
59+
let flags = fcntl(&stream, FcntlArg::F_GETFL).unwrap();
60+
assert!(!OFlag::from_bits_retain(flags).contains(OFlag::O_NONBLOCK));
61+
}
62+
}

packages/cli/binding/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ mod package_manager;
2828
#[allow(dead_code)]
2929
mod utils;
3030

31-
use std::{collections::HashMap, error::Error as StdError, ffi::OsStr, fmt::Write as _, sync::Arc};
31+
use std::{
32+
collections::HashMap,
33+
error::Error as StdError,
34+
ffi::OsStr,
35+
fmt::Write as _,
36+
sync::{Arc, Once},
37+
};
3238

3339
use napi::{anyhow, bindgen_prelude::*, threadsafe_function::ThreadsafeFunction};
3440
use napi_derive::napi;
@@ -151,6 +157,9 @@ fn format_error_message(error: &(dyn StdError + 'static)) -> String {
151157
/// and process JavaScript callbacks (via ThreadsafeFunction).
152158
#[napi]
153159
pub async fn run(options: CliOptions) -> Result<i32> {
160+
static ENSURE_BLOCKING_STDIO: Once = Once::new();
161+
ENSURE_BLOCKING_STDIO.call_once(vite_shared::ensure_blocking_stdio);
162+
154163
// Use provided cwd or current directory
155164
let mut cwd = current_dir()?;
156165
if let Some(options_cwd) = options.cwd {

0 commit comments

Comments
 (0)