From 2bc7e1e23b83de323e12f1a3798ddd5d19a63d77 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:12:54 +0800 Subject: [PATCH 1/4] fix(cli): handle stdout backpressure --- crates/vite_shared/src/output.rs | 86 +++++++++++++++++++++- packages/cli/binding/src/check/analysis.rs | 27 ++++--- packages/cli/binding/src/check/mod.rs | 20 ++--- 3 files changed, 110 insertions(+), 23 deletions(-) diff --git a/crates/vite_shared/src/output.rs b/crates/vite_shared/src/output.rs index c3bda52e4c..bcda2b3339 100644 --- a/crates/vite_shared/src/output.rs +++ b/crates/vite_shared/src/output.rs @@ -3,7 +3,12 @@ //! All commands should use these functions instead of ad-hoc formatting to ensure //! consistent output across the entire CLI. -use std::sync::atomic::{AtomicBool, Ordering}; +use std::{ + io::{self, Write}, + sync::atomic::{AtomicBool, Ordering}, + thread, + time::Duration, +}; use owo_colors::OwoColorize; @@ -112,3 +117,82 @@ pub fn raw_inline(msg: &str) { pub fn raw_stderr(msg: &str) { eprintln!("{msg}"); } + +/// Write the complete buffer, retrying when the output is temporarily unavailable. +pub fn write_all_with_backpressure( + writer: &mut W, + mut buf: &[u8], +) -> io::Result<()> { + while !buf.is_empty() { + match writer.write(buf) { + Ok(0) => return Err(io::ErrorKind::WriteZero.into()), + Ok(written) => buf = &buf[written..], + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) if error.kind() == io::ErrorKind::WouldBlock => { + thread::sleep(Duration::from_millis(1)); + } + Err(error) => return Err(error), + } + } + + Ok(()) +} + +/// Print a raw message while returning output errors to the caller. +pub fn try_raw(msg: &str) -> io::Result<()> { + if user_output_to_stderr() { + write_line_with_backpressure(&mut io::stderr().lock(), msg) + } else { + write_line_with_backpressure(&mut io::stdout().lock(), msg) + } +} + +fn write_line_with_backpressure(writer: &mut W, msg: &str) -> io::Result<()> { + write_all_with_backpressure(writer, msg.as_bytes())?; + write_all_with_backpressure(writer, b"\n") +} + +#[cfg(test)] +mod tests { + use std::io::{self, Write}; + + use super::write_all_with_backpressure; + + #[derive(Default)] + struct BackpressuredWriter { + bytes: Vec, + writes: usize, + } + + impl Write for BackpressuredWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.writes += 1; + match self.writes { + 1 => { + let written = buf.len().min(4); + self.bytes.extend_from_slice(&buf[..written]); + Ok(written) + } + 2 | 3 => Err(io::Error::from(io::ErrorKind::WouldBlock)), + _ => { + self.bytes.extend_from_slice(buf); + Ok(buf.len()) + } + } + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + #[test] + fn retries_writes_when_output_temporarily_would_block() { + let mut writer = BackpressuredWriter::default(); + + write_all_with_backpressure(&mut writer, b"complete diagnostics").unwrap(); + + assert_eq!(writer.bytes, b"complete diagnostics"); + assert_eq!(writer.writes, 4); + } +} diff --git a/packages/cli/binding/src/check/analysis.rs b/packages/cli/binding/src/check/analysis.rs index e02d26fb54..50b6a388da 100644 --- a/packages/cli/binding/src/check/analysis.rs +++ b/packages/cli/binding/src/check/analysis.rs @@ -127,20 +127,19 @@ pub(super) fn format_count(count: usize, singular: &str, plural: &str) -> String if count == 1 { format!("1 {singular}") } else { format!("{count} {plural}") } } -pub(super) fn print_stdout_block(block: &str) { +pub(super) fn print_stdout_block(block: &str) -> std::io::Result<()> { let trimmed = block.trim_matches('\n'); if trimmed.is_empty() { - return; + return Ok(()); } - use std::io::Write; let mut stdout = std::io::stdout().lock(); - let _ = stdout.write_all(trimmed.as_bytes()); - let _ = stdout.write_all(b"\n"); + output::write_all_with_backpressure(&mut stdout, trimmed.as_bytes())?; + output::write_all_with_backpressure(&mut stdout, b"\n") } -pub(super) fn print_summary_line(message: &str) { - output::raw(""); +pub(super) fn print_summary_line(message: &str) -> std::io::Result<()> { + output::try_raw("")?; if std::io::stdout().is_terminal() && message.contains('`') { let mut formatted = String::with_capacity(message.len()); let mut segments = message.split('`'); @@ -156,18 +155,22 @@ pub(super) fn print_summary_line(message: &str) { } is_accent = !is_accent; } - output::raw(&formatted); + output::try_raw(&formatted) } else { - output::raw(message); + output::try_raw(message) } } -pub(super) fn print_error_block(error_msg: &str, combined_output: &str, summary_msg: &str) { +pub(super) fn print_error_block( + error_msg: &str, + combined_output: &str, + summary_msg: &str, +) -> std::io::Result<()> { output::error(error_msg); if !combined_output.trim().is_empty() { - print_stdout_block(combined_output); + print_stdout_block(combined_output)?; } - print_summary_line(summary_msg); + print_summary_line(summary_msg) } pub(super) fn print_pass_line(message: &str, detail: Option<&str>) { diff --git a/packages/cli/binding/src/check/mod.rs b/packages/cli/binding/src/check/mod.rs index 1272be9460..4d33d8df2d 100644 --- a/packages/cli/binding/src/check/mod.rs +++ b/packages/cli/binding/src/check/mod.rs @@ -61,7 +61,7 @@ pub(crate) async fn execute_check( output::error("No checks enabled"); print_summary_line( "Enable `lint.options.typeCheck` in vite.config.ts for type-check only, drop a `--no-fmt`/`--no-lint` flag, or re-enable `check.fmt`/`check.lint` in vite.config.ts.", - ); + )?; return Ok(ExitStatus(1)); } @@ -103,13 +103,13 @@ pub(crate) async fn execute_check( ), Some(Err(failure)) => { output::error("Formatting issues found"); - print_stdout_block(&failure.issue_files.join("\n")); + print_stdout_block(&failure.issue_files.join("\n"))?; print_summary_line(&format!( "Found formatting issues in {} ({}, {} threads). Run `vp check --fix` to fix them.", format_count(failure.issue_count, "file", "files"), failure.summary.duration, failure.summary.threads - )); + ))?; } None => { // oxfmt handles --no-error-on-unmatched-pattern natively and @@ -121,7 +121,7 @@ pub(crate) async fn execute_check( "Formatting could not start", &combined_output, "Formatting failed before analysis started", - ); + )?; } } } @@ -139,7 +139,7 @@ pub(crate) async fn execute_check( "Formatting could not complete", &combined_output, "Formatting failed during fix", - ); + )?; } return Ok(status); } @@ -202,7 +202,7 @@ pub(crate) async fn execute_check( } else { output::error(lint_message_kind.issue_heading()); } - print_stdout_block(&failure.diagnostics); + print_stdout_block(&failure.diagnostics)?; print_summary_line(&format!( "Found {} and {} in {} ({}, {} threads)", format_count(failure.errors, "error", "errors"), @@ -210,7 +210,7 @@ pub(crate) async fn execute_check( format_count(failure.summary.files, "file", "files"), failure.summary.duration, failure.summary.threads - )); + ))?; } None => { // oxlint handles --no-error-on-unmatched-pattern natively and @@ -220,9 +220,9 @@ pub(crate) async fn execute_check( if !(suppress_unmatched && status == ExitStatus::SUCCESS) { output::error("Linting could not start"); if !combined_output.trim().is_empty() { - print_stdout_block(&combined_output); + print_stdout_block(&combined_output)?; } - print_summary_line("Linting failed before analysis started"); + print_summary_line("Linting failed before analysis started")?; } } } @@ -262,7 +262,7 @@ pub(crate) async fn execute_check( "Formatting could not finish after lint fixes", &combined_output, "Formatting failed after lint fixes were applied", - ); + )?; return Ok(status); } flush_deferred_pass_lines(&mut fmt_fix_started, &mut deferred_lint_pass); From 227560d5dee4103027ab9ded7e6398c741d15305 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:12:55 +0800 Subject: [PATCH 2/4] fix(cli): harden stdout backpressure --- crates/vite_shared/src/output.rs | 236 ++++++++++++++++++++- packages/cli/binding/src/check/analysis.rs | 10 +- packages/cli/binding/src/check/mod.rs | 43 ++-- 3 files changed, 255 insertions(+), 34 deletions(-) diff --git a/crates/vite_shared/src/output.rs b/crates/vite_shared/src/output.rs index bcda2b3339..7bb284f15c 100644 --- a/crates/vite_shared/src/output.rs +++ b/crates/vite_shared/src/output.rs @@ -3,14 +3,19 @@ //! All commands should use these functions instead of ad-hoc formatting to ensure //! consistent output across the entire CLI. +#[cfg(unix)] +use std::os::fd::{AsFd, BorrowedFd}; use std::{ io::{self, Write}, sync::atomic::{AtomicBool, Ordering}, - thread, - time::Duration, }; +#[cfg(not(unix))] +use std::{thread, time::Duration}; +#[cfg(unix)] +use nix::poll::{PollFd, PollFlags, PollTimeout, poll}; use owo_colors::OwoColorize; +use vite_str::format; /// When set, user-facing stdout output (info/pass/note/success/raw) is routed /// to stderr instead. Shim dispatch enables this once at entry: a shim's @@ -119,17 +124,39 @@ pub fn raw_stderr(msg: &str) { } /// Write the complete buffer, retrying when the output is temporarily unavailable. -pub fn write_all_with_backpressure( +#[cfg(unix)] +fn write_all_with_backpressure( writer: &mut W, - mut buf: &[u8], + buf: &[u8], ) -> io::Result<()> { + write_all_with_backpressure_inner(writer, buf, |writer| wait_until_writable(writer.as_fd())) +} + +/// Write the complete buffer, retrying when the output is temporarily unavailable. +#[cfg(not(unix))] +fn write_all_with_backpressure(writer: &mut W, buf: &[u8]) -> io::Result<()> { + write_all_with_backpressure_inner(writer, buf, |_| { + thread::sleep(Duration::from_millis(1)); + Ok(()) + }) +} + +fn write_all_with_backpressure_inner( + writer: &mut W, + mut buf: &[u8], + mut wait_until_writable: F, +) -> io::Result<()> +where + W: Write + ?Sized, + F: FnMut(&W) -> io::Result<()>, +{ while !buf.is_empty() { match writer.write(buf) { Ok(0) => return Err(io::ErrorKind::WriteZero.into()), Ok(written) => buf = &buf[written..], Err(error) if error.kind() == io::ErrorKind::Interrupted => {} Err(error) if error.kind() == io::ErrorKind::WouldBlock => { - thread::sleep(Duration::from_millis(1)); + wait_until_writable(writer)?; } Err(error) => return Err(error), } @@ -138,15 +165,56 @@ pub fn write_all_with_backpressure( Ok(()) } +#[cfg(unix)] +fn wait_until_writable(fd: BorrowedFd<'_>) -> io::Result<()> { + let mut fds = [PollFd::new(fd, PollFlags::POLLOUT)]; + loop { + match poll(&mut fds, PollTimeout::NONE) { + Ok(_) => return Ok(()), + Err(nix::errno::Errno::EINTR) => {} + Err(error) => return Err(io::Error::from(error)), + } + } +} + /// Print a raw message while returning output errors to the caller. pub fn try_raw(msg: &str) -> io::Result<()> { + try_user_line(msg) +} + +/// Print a raw message to stdout while returning output errors to the caller. +pub fn try_raw_stdout(msg: &str) -> io::Result<()> { + write_line_with_backpressure(&mut io::stdout().lock(), msg) +} + +/// Print a pass message while returning output errors to the caller. +pub fn try_pass(msg: &str) -> io::Result<()> { + try_user_line(&format!("{} {msg}", "pass:".bright_blue().bold())) +} + +/// Print a note message while returning output errors to the caller. +pub fn try_note(msg: &str) -> io::Result<()> { + try_user_line(&format!("{} {msg}", "note:".dimmed().bold())) +} + +fn try_user_line(msg: &str) -> io::Result<()> { if user_output_to_stderr() { write_line_with_backpressure(&mut io::stderr().lock(), msg) } else { - write_line_with_backpressure(&mut io::stdout().lock(), msg) + try_raw_stdout(msg) } } +#[cfg(unix)] +fn write_line_with_backpressure( + writer: &mut W, + msg: &str, +) -> io::Result<()> { + write_all_with_backpressure(writer, msg.as_bytes())?; + write_all_with_backpressure(writer, b"\n") +} + +#[cfg(not(unix))] fn write_line_with_backpressure(writer: &mut W, msg: &str) -> io::Result<()> { write_all_with_backpressure(writer, msg.as_bytes())?; write_all_with_backpressure(writer, b"\n") @@ -156,7 +224,7 @@ fn write_line_with_backpressure(writer: &mut W, msg: &str) -> mod tests { use std::io::{self, Write}; - use super::write_all_with_backpressure; + use super::{write_all_with_backpressure, write_all_with_backpressure_inner}; #[derive(Default)] struct BackpressuredWriter { @@ -164,6 +232,16 @@ mod tests { writes: usize, } + #[derive(Default)] + struct InterruptedWriter { + bytes: Vec, + writes: usize, + } + + struct FailingWriter { + kind: io::ErrorKind, + } + impl Write for BackpressuredWriter { fn write(&mut self, buf: &[u8]) -> io::Result { self.writes += 1; @@ -186,13 +264,155 @@ mod tests { } } + impl Write for InterruptedWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.writes += 1; + if self.writes == 1 { + return Err(io::ErrorKind::Interrupted.into()); + } + self.bytes.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + impl Write for FailingWriter { + fn write(&mut self, _buf: &[u8]) -> io::Result { + if self.kind == io::ErrorKind::WriteZero { Ok(0) } else { Err(self.kind.into()) } + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + #[test] fn retries_writes_when_output_temporarily_would_block() { let mut writer = BackpressuredWriter::default(); + let mut waits = 0; - write_all_with_backpressure(&mut writer, b"complete diagnostics").unwrap(); + write_all_with_backpressure_inner(&mut writer, b"complete diagnostics", |_| { + waits += 1; + Ok(()) + }) + .unwrap(); assert_eq!(writer.bytes, b"complete diagnostics"); assert_eq!(writer.writes, 4); + assert_eq!(waits, 2); + } + + #[test] + fn retries_interrupted_writes_without_waiting_for_readiness() { + let mut writer = InterruptedWriter::default(); + + write_all_with_backpressure_inner(&mut writer, b"complete diagnostics", |_| { + panic!("interrupted writes should retry immediately") + }) + .unwrap(); + + assert_eq!(writer.bytes, b"complete diagnostics"); + assert_eq!(writer.writes, 2); + } + + #[test] + fn returns_write_zero_when_the_writer_makes_no_progress() { + let mut writer = FailingWriter { kind: io::ErrorKind::WriteZero }; + + let error = write_all_with_backpressure_inner(&mut writer, b"diagnostics", |_| { + panic!("write zero should not wait for readiness") + }) + .unwrap_err(); + + assert_eq!(error.kind(), io::ErrorKind::WriteZero); + } + + #[test] + fn returns_non_retryable_write_errors() { + let mut writer = FailingWriter { kind: io::ErrorKind::BrokenPipe }; + + let error = write_all_with_backpressure_inner(&mut writer, b"diagnostics", |_| { + panic!("broken pipes should not wait for readiness") + }) + .unwrap_err(); + + assert_eq!(error.kind(), io::ErrorKind::BrokenPipe); + } + + #[cfg(unix)] + #[test] + fn waits_until_a_nonblocking_writer_is_ready_before_retrying() { + use std::{ + io::Read, + net::Shutdown, + os::{ + fd::{AsFd, BorrowedFd}, + unix::net::UnixStream, + }, + sync::mpsc, + thread, + }; + + struct CountingWriter { + stream: UnixStream, + writes: usize, + notify_would_block: Option>, + } + + impl Write for CountingWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.writes += 1; + let result = self.stream.write(buf); + if matches!(&result, Err(error) if error.kind() == io::ErrorKind::WouldBlock) + && let Some(sender) = self.notify_would_block.take() + { + sender.send(()).unwrap(); + } + result + } + + fn flush(&mut self) -> io::Result<()> { + self.stream.flush() + } + } + + impl AsFd for CountingWriter { + fn as_fd(&self) -> BorrowedFd<'_> { + self.stream.as_fd() + } + } + + let (writer, mut reader) = UnixStream::pair().unwrap(); + writer.set_nonblocking(true).unwrap(); + let mut writer = CountingWriter { stream: writer, writes: 0, notify_would_block: None }; + let fill = [0_u8; 8192]; + loop { + match writer.write(&fill) { + Ok(0) => panic!("nonblocking stream stopped accepting bytes without an error"), + Ok(_) => {} + Err(error) if error.kind() == io::ErrorKind::WouldBlock => break, + Err(error) => panic!("failed to fill nonblocking stream: {error}"), + } + } + writer.writes = 0; + let (would_block_tx, would_block_rx) = mpsc::channel(); + writer.notify_would_block = Some(would_block_tx); + + let reader_thread = thread::spawn(move || { + would_block_rx.recv().unwrap(); + let mut received = Vec::new(); + reader.read_to_end(&mut received).unwrap(); + received + }); + + write_all_with_backpressure(&mut writer, b"complete diagnostics").unwrap(); + writer.stream.shutdown(Shutdown::Write).unwrap(); + + let received = reader_thread.join().unwrap(); + assert!(received.ends_with(b"complete diagnostics")); + assert!((2..=5).contains(&writer.writes), "retried write {} times", writer.writes); } } diff --git a/packages/cli/binding/src/check/analysis.rs b/packages/cli/binding/src/check/analysis.rs index 50b6a388da..f9f801d6c9 100644 --- a/packages/cli/binding/src/check/analysis.rs +++ b/packages/cli/binding/src/check/analysis.rs @@ -133,9 +133,7 @@ pub(super) fn print_stdout_block(block: &str) -> std::io::Result<()> { return Ok(()); } - let mut stdout = std::io::stdout().lock(); - output::write_all_with_backpressure(&mut stdout, trimmed.as_bytes())?; - output::write_all_with_backpressure(&mut stdout, b"\n") + output::try_raw_stdout(trimmed) } pub(super) fn print_summary_line(message: &str) -> std::io::Result<()> { @@ -173,11 +171,11 @@ pub(super) fn print_error_block( print_summary_line(summary_msg) } -pub(super) fn print_pass_line(message: &str, detail: Option<&str>) { +pub(super) fn print_pass_line(message: &str, detail: Option<&str>) -> std::io::Result<()> { if let Some(detail) = detail { - output::raw(&format!("{} {message} {}", "pass:".bright_blue().bold(), detail.dimmed())); + output::try_raw(&format!("{} {message} {}", "pass:".bright_blue().bold(), detail.dimmed())) } else { - output::pass(message); + output::try_pass(message) } } diff --git a/packages/cli/binding/src/check/mod.rs b/packages/cli/binding/src/check/mod.rs index 4d33d8df2d..bc6f2582a4 100644 --- a/packages/cli/binding/src/check/mod.rs +++ b/packages/cli/binding/src/check/mod.rs @@ -45,10 +45,10 @@ pub(crate) async fn execute_check( let config_fmt_off = !json_bool(resolved_vite_config.check.as_ref(), "fmt", true); let config_lint_off = !json_bool(resolved_vite_config.check.as_ref(), "lint", true); if config_fmt_off && !no_fmt_flag { - output::note("Format skipped (check.fmt: false in vite.config.ts)"); + output::try_note("Format skipped (check.fmt: false in vite.config.ts)")?; } if config_lint_off && !no_lint_flag { - output::note("Lint skipped (check.lint: false in vite.config.ts)"); + output::try_note("Lint skipped (check.lint: false in vite.config.ts)")?; } let no_fmt = no_fmt_flag || config_fmt_off; let no_lint = no_lint_flag || config_lint_off; @@ -91,16 +91,18 @@ pub(crate) async fn execute_check( if !fix { match analyze_fmt_check_output(&combined_output) { - Some(Ok(success)) => print_pass_line( - &format!( - "All {} are correctly formatted", - format_count(success.summary.files, "file", "files") - ), - Some(&format!( - "({}, {} threads)", - success.summary.duration, success.summary.threads - )), - ), + Some(Ok(success)) => { + print_pass_line( + &format!( + "All {} are correctly formatted", + format_count(success.summary.files, "file", "files") + ), + Some(&format!( + "({}, {} threads)", + success.summary.duration, success.summary.threads + )), + )?; + } Some(Err(failure)) => { output::error("Formatting issues found"); print_stdout_block(&failure.issue_files.join("\n"))?; @@ -131,7 +133,7 @@ pub(crate) async fn execute_check( print_pass_line( "Formatting completed for checked files", Some(&format!("({})", format_elapsed(fmt_start.elapsed()))), - ); + )?; } if status != ExitStatus::SUCCESS { if fix { @@ -193,7 +195,7 @@ pub(crate) async fn execute_check( if fix && !no_fmt { deferred_lint_pass = Some((message, detail)); } else { - print_pass_line(&message, Some(&detail)); + print_pass_line(&message, Some(&detail))?; } } Some(Err(failure)) => { @@ -230,7 +232,7 @@ pub(crate) async fn execute_check( // Surface fmt `--fix` completion before bailing so users can see // the working tree was mutated before the lint/type-check error. if fix && !no_fmt { - flush_deferred_pass_lines(&mut fmt_fix_started, &mut deferred_lint_pass); + flush_deferred_pass_lines(&mut fmt_fix_started, &mut deferred_lint_pass)?; } return Ok(status); } @@ -265,13 +267,13 @@ pub(crate) async fn execute_check( )?; return Ok(status); } - flush_deferred_pass_lines(&mut fmt_fix_started, &mut deferred_lint_pass); + flush_deferred_pass_lines(&mut fmt_fix_started, &mut deferred_lint_pass)?; } // Type-check-only mode skips the re-fmt block above, so flush deferred // pass lines here. if fix && !no_fmt && run_lint_phase && !lint_enabled { - flush_deferred_pass_lines(&mut fmt_fix_started, &mut deferred_lint_pass); + flush_deferred_pass_lines(&mut fmt_fix_started, &mut deferred_lint_pass)?; } Ok(status) @@ -280,16 +282,17 @@ pub(crate) async fn execute_check( fn flush_deferred_pass_lines( fmt_fix_started: &mut Option, deferred_lint_pass: &mut Option<(String, String)>, -) { +) -> std::io::Result<()> { if let Some(started) = fmt_fix_started.take() { print_pass_line( "Formatting completed for checked files", Some(&format!("({})", format_elapsed(started.elapsed()))), - ); + )?; } if let Some((message, detail)) = deferred_lint_pass.take() { - print_pass_line(&message, Some(&detail)); + print_pass_line(&message, Some(&detail))?; } + Ok(()) } /// Combine stdout and stderr from a captured command output. From 47572812cbe87dbcd9ce6b5e5cd8a3dd96d9871f Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:12:55 +0800 Subject: [PATCH 3/4] test(cli): simplify stdout backpressure coverage --- crates/vite_shared/src/output.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/vite_shared/src/output.rs b/crates/vite_shared/src/output.rs index 7bb284f15c..7122ff4eee 100644 --- a/crates/vite_shared/src/output.rs +++ b/crates/vite_shared/src/output.rs @@ -356,15 +356,13 @@ mod tests { thread, }; - struct CountingWriter { + struct NotifyingWriter { stream: UnixStream, - writes: usize, notify_would_block: Option>, } - impl Write for CountingWriter { + impl Write for NotifyingWriter { fn write(&mut self, buf: &[u8]) -> io::Result { - self.writes += 1; let result = self.stream.write(buf); if matches!(&result, Err(error) if error.kind() == io::ErrorKind::WouldBlock) && let Some(sender) = self.notify_would_block.take() @@ -379,7 +377,7 @@ mod tests { } } - impl AsFd for CountingWriter { + impl AsFd for NotifyingWriter { fn as_fd(&self) -> BorrowedFd<'_> { self.stream.as_fd() } @@ -387,7 +385,7 @@ mod tests { let (writer, mut reader) = UnixStream::pair().unwrap(); writer.set_nonblocking(true).unwrap(); - let mut writer = CountingWriter { stream: writer, writes: 0, notify_would_block: None }; + let mut writer = NotifyingWriter { stream: writer, notify_would_block: None }; let fill = [0_u8; 8192]; loop { match writer.write(&fill) { @@ -397,7 +395,6 @@ mod tests { Err(error) => panic!("failed to fill nonblocking stream: {error}"), } } - writer.writes = 0; let (would_block_tx, would_block_rx) = mpsc::channel(); writer.notify_would_block = Some(would_block_tx); @@ -413,6 +410,5 @@ mod tests { let received = reader_thread.join().unwrap(); assert!(received.ends_with(b"complete diagnostics")); - assert!((2..=5).contains(&writer.writes), "retried write {} times", writer.writes); } } From 1458b01027a84186f90cc2cef566e441d0eab6ae Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:21:24 +0800 Subject: [PATCH 4/4] test(cli): gate Unix-only stdout test import --- crates/vite_shared/src/output.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/vite_shared/src/output.rs b/crates/vite_shared/src/output.rs index 7122ff4eee..0d47f8c796 100644 --- a/crates/vite_shared/src/output.rs +++ b/crates/vite_shared/src/output.rs @@ -224,7 +224,9 @@ fn write_line_with_backpressure(writer: &mut W, msg: &str) -> mod tests { use std::io::{self, Write}; - use super::{write_all_with_backpressure, write_all_with_backpressure_inner}; + #[cfg(unix)] + use super::write_all_with_backpressure; + use super::write_all_with_backpressure_inner; #[derive(Default)] struct BackpressuredWriter {