Skip to content

Commit c666944

Browse files
stdio: make stdout block-buffered when not associated to a terminal
1 parent 292be5c commit c666944

File tree

6 files changed

+251
-16
lines changed

6 files changed

+251
-16
lines changed

library/std/src/io/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ mod util;
346346

347347
const DEFAULT_BUF_SIZE: usize = crate::sys::io::DEFAULT_BUF_SIZE;
348348

349-
pub(crate) use stdio::cleanup;
349+
pub(crate) use stdio::{StderrRaw, StdinRaw, StdoutRaw, cleanup};
350350

351351
struct Guard<'a> {
352352
buf: &'a mut Vec<u8>,

library/std/src/io/stdio.rs

Lines changed: 95 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use crate::fmt;
88
use crate::fs::File;
99
use crate::io::prelude::*;
1010
use crate::io::{
11-
self, BorrowedCursor, BufReader, IoSlice, IoSliceMut, LineWriter, Lines, SpecReadByte,
11+
self, BorrowedCursor, BufReader, BufWriter, IoSlice, IoSliceMut, LineWriter, Lines,
12+
SpecReadByte,
1213
};
1314
use crate::panic::{RefUnwindSafe, UnwindSafe};
1415
use crate::sync::atomic::{Atomic, AtomicBool, Ordering};
@@ -43,19 +44,19 @@ static OUTPUT_CAPTURE_USED: Atomic<bool> = AtomicBool::new(false);
4344
///
4445
/// This handle is not synchronized or buffered in any fashion. Constructed via
4546
/// the `std::io::stdio::stdin_raw` function.
46-
struct StdinRaw(stdio::Stdin);
47+
pub(crate) struct StdinRaw(stdio::Stdin);
4748

4849
/// A handle to a raw instance of the standard output stream of this process.
4950
///
5051
/// This handle is not synchronized or buffered in any fashion. Constructed via
5152
/// the `std::io::stdio::stdout_raw` function.
52-
struct StdoutRaw(stdio::Stdout);
53+
pub(crate) struct StdoutRaw(stdio::Stdout);
5354

5455
/// A handle to a raw instance of the standard output stream of this process.
5556
///
5657
/// This handle is not synchronized or buffered in any fashion. Constructed via
5758
/// the `std::io::stdio::stderr_raw` function.
58-
struct StderrRaw(stdio::Stderr);
59+
pub(crate) struct StderrRaw(stdio::Stderr);
5960

6061
/// Constructs a new raw handle to the standard input of this process.
6162
///
@@ -576,6 +577,76 @@ impl fmt::Debug for StdinLock<'_> {
576577
}
577578
}
578579

580+
/// A buffered writer for stdout and stderr.
581+
///
582+
/// This writer may be either [line-buffered](LineWriter) or [block-buffered](BufWriter), depending
583+
/// on whether the underlying file is a terminal or not.
584+
#[derive(Debug)]
585+
enum StdioBufWriter<W: Write> {
586+
LineBuffered(LineWriter<W>),
587+
BlockBuffered(BufWriter<W>),
588+
}
589+
590+
impl<W: Write + IsTerminal> StdioBufWriter<W> {
591+
/// Wraps a writer using the most appropriate buffering method.
592+
///
593+
/// If `w` is a terminal, then the resulting `StdioBufWriter` will be line-buffered, otherwise
594+
/// it will be block-buffered.
595+
fn new(w: W) -> Self {
596+
if w.is_terminal() {
597+
Self::LineBuffered(LineWriter::new(w))
598+
} else {
599+
Self::BlockBuffered(BufWriter::new(w))
600+
}
601+
}
602+
}
603+
604+
impl<W: Write> StdioBufWriter<W> {
605+
/// Wraps a writer using a block-buffer with the given capacity.
606+
fn with_capacity(cap: usize, w: W) -> Self {
607+
Self::BlockBuffered(BufWriter::with_capacity(cap, w))
608+
}
609+
}
610+
611+
impl<W: Write> Write for StdioBufWriter<W> {
612+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
613+
match self {
614+
Self::LineBuffered(w) => w.write(buf),
615+
Self::BlockBuffered(w) => w.write(buf),
616+
}
617+
}
618+
fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
619+
match self {
620+
Self::LineBuffered(w) => w.write_vectored(bufs),
621+
Self::BlockBuffered(w) => w.write_vectored(bufs),
622+
}
623+
}
624+
fn is_write_vectored(&self) -> bool {
625+
match self {
626+
Self::LineBuffered(w) => w.is_write_vectored(),
627+
Self::BlockBuffered(w) => w.is_write_vectored(),
628+
}
629+
}
630+
fn flush(&mut self) -> io::Result<()> {
631+
match self {
632+
Self::LineBuffered(w) => w.flush(),
633+
Self::BlockBuffered(w) => w.flush(),
634+
}
635+
}
636+
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
637+
match self {
638+
Self::LineBuffered(w) => w.write_all(buf),
639+
Self::BlockBuffered(w) => w.write_all(buf),
640+
}
641+
}
642+
fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> io::Result<()> {
643+
match self {
644+
Self::LineBuffered(w) => w.write_all_vectored(bufs),
645+
Self::BlockBuffered(w) => w.write_all_vectored(bufs),
646+
}
647+
}
648+
}
649+
579650
/// A handle to the global standard output stream of the current process.
580651
///
581652
/// Each handle shares a global buffer of data to be written to the standard
@@ -606,10 +677,9 @@ impl fmt::Debug for StdinLock<'_> {
606677
/// [`io::stdout`]: stdout
607678
#[stable(feature = "rust1", since = "1.0.0")]
608679
pub struct Stdout {
609-
// FIXME: this should be LineWriter or BufWriter depending on the state of
610-
// stdout (tty or not). Note that if this is not line buffered it
611-
// should also flush-on-panic or some form of flush-on-abort.
612-
inner: &'static ReentrantLock<RefCell<LineWriter<StdoutRaw>>>,
680+
// FIXME: if this is not line buffered it should flush-on-panic or some
681+
// form of flush-on-abort.
682+
inner: &'static ReentrantLock<RefCell<StdioBufWriter<StdoutRaw>>>,
613683
}
614684

615685
/// A locked reference to the [`Stdout`] handle.
@@ -638,10 +708,10 @@ pub struct Stdout {
638708
#[must_use = "if unused stdout will immediately unlock"]
639709
#[stable(feature = "rust1", since = "1.0.0")]
640710
pub struct StdoutLock<'a> {
641-
inner: ReentrantLockGuard<'a, RefCell<LineWriter<StdoutRaw>>>,
711+
inner: ReentrantLockGuard<'a, RefCell<StdioBufWriter<StdoutRaw>>>,
642712
}
643713

644-
static STDOUT: OnceLock<ReentrantLock<RefCell<LineWriter<StdoutRaw>>>> = OnceLock::new();
714+
static STDOUT: OnceLock<ReentrantLock<RefCell<StdioBufWriter<StdoutRaw>>>> = OnceLock::new();
645715

646716
/// Constructs a new handle to the standard output of the current process.
647717
///
@@ -716,7 +786,7 @@ static STDOUT: OnceLock<ReentrantLock<RefCell<LineWriter<StdoutRaw>>>> = OnceLoc
716786
pub fn stdout() -> Stdout {
717787
Stdout {
718788
inner: STDOUT
719-
.get_or_init(|| ReentrantLock::new(RefCell::new(LineWriter::new(stdout_raw())))),
789+
.get_or_init(|| ReentrantLock::new(RefCell::new(StdioBufWriter::new(stdout_raw())))),
720790
}
721791
}
722792

@@ -727,7 +797,7 @@ pub fn cleanup() {
727797
let mut initialized = false;
728798
let stdout = STDOUT.get_or_init(|| {
729799
initialized = true;
730-
ReentrantLock::new(RefCell::new(LineWriter::with_capacity(0, stdout_raw())))
800+
ReentrantLock::new(RefCell::new(StdioBufWriter::with_capacity(0, stdout_raw())))
731801
});
732802

733803
if !initialized {
@@ -736,7 +806,7 @@ pub fn cleanup() {
736806
// might have leaked a StdoutLock, which would
737807
// otherwise cause a deadlock here.
738808
if let Some(lock) = stdout.try_lock() {
739-
*lock.borrow_mut() = LineWriter::with_capacity(0, stdout_raw());
809+
*lock.borrow_mut() = StdioBufWriter::with_capacity(0, stdout_raw());
740810
}
741811
}
742812
}
@@ -1262,7 +1332,18 @@ macro_rules! impl_is_terminal {
12621332
)*}
12631333
}
12641334

1265-
impl_is_terminal!(File, Stdin, StdinLock<'_>, Stdout, StdoutLock<'_>, Stderr, StderrLock<'_>);
1335+
impl_is_terminal!(
1336+
File,
1337+
Stdin,
1338+
StdinLock<'_>,
1339+
StdinRaw,
1340+
Stdout,
1341+
StdoutLock<'_>,
1342+
StdoutRaw,
1343+
Stderr,
1344+
StderrLock<'_>,
1345+
StderrRaw,
1346+
);
12661347

12671348
#[unstable(
12681349
feature = "print_internals",

library/std/src/io/stdio/tests.rs

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use super::*;
2+
use crate::assert_matches::assert_matches;
3+
use crate::os::fd::{FromRawFd, OwnedFd};
24
use crate::panic::{RefUnwindSafe, UnwindSafe};
3-
use crate::sync::mpsc::sync_channel;
5+
use crate::sync::mpsc::{TryRecvError, sync_channel};
46
use crate::thread;
57

68
#[test]
@@ -164,3 +166,95 @@ where
164166
[Start1, Acquire1, Start2, Release1, Acquire2, Release2, Acquire1, Release1]
165167
);
166168
}
169+
170+
#[test]
171+
#[cfg(unix)]
172+
fn stdiobufwriter_line_buffered() {
173+
let mut fd1 = -1;
174+
let mut fd2 = -1;
175+
176+
if unsafe {
177+
libc::openpty(
178+
&raw mut fd1,
179+
&raw mut fd2,
180+
crate::ptr::null_mut(),
181+
crate::ptr::null(),
182+
crate::ptr::null(),
183+
)
184+
} < 0
185+
{
186+
panic!("openpty() failed with {:?}", io::Error::last_os_error());
187+
}
188+
189+
let writer = unsafe { File::from_raw_fd(fd1) };
190+
let mut reader = unsafe { File::from_raw_fd(fd2) };
191+
192+
assert!(writer.is_terminal(), "file descriptor returned by openpty() must be a terminal");
193+
assert!(reader.is_terminal(), "file descriptor returned by openpty() must be a terminal");
194+
195+
let (sender, receiver) = sync_channel(64);
196+
197+
thread::spawn(move || {
198+
loop {
199+
let mut buf = vec![0u8; 1024];
200+
let size = reader.read(&mut buf[..]).expect("read failed");
201+
buf.truncate(size);
202+
sender.send(buf);
203+
}
204+
});
205+
206+
let mut writer = StdioBufWriter::new(writer);
207+
assert_matches!(
208+
writer,
209+
StdioBufWriter::LineBuffered(_),
210+
"StdioBufWriter should be line-buffered when created from a terminal"
211+
);
212+
213+
writer.write_all(b"line 1\n").expect("write failed");
214+
assert_eq!(receiver.recv().expect("recv failed"), b"line 1\n");
215+
216+
writer.write_all(b"line 2\nextra ").expect("write failed");
217+
assert_eq!(receiver.recv().expect("recv failed"), b"line 2\n");
218+
219+
writer.write_all(b"line 3\n").expect("write failed");
220+
assert_eq!(receiver.recv().expect("recv failed"), b"extra line 3\n");
221+
}
222+
223+
#[test]
224+
fn stdiobufwriter_block_buffered() {
225+
let (mut reader, mut writer) = io::pipe().expect("pipe() failed");
226+
227+
// Need to convert to an OwnedFd and then into a File because PipeReader/PipeWriter don't
228+
// implement IsTerminal, but that is required by StdioBufWriter
229+
let mut reader = File::from(OwnedFd::from(reader));
230+
let mut writer = File::from(OwnedFd::from(writer));
231+
232+
assert!(!reader.is_terminal(), "file returned by pipe() cannot be a terminal");
233+
assert!(!writer.is_terminal(), "file returned by pipe() cannot be a terminal");
234+
235+
let (sender, receiver) = sync_channel(64);
236+
237+
thread::spawn(move || {
238+
loop {
239+
let mut buf = vec![0u8; 1024];
240+
let size = reader.read(&mut buf[..]).expect("read failed");
241+
buf.truncate(size);
242+
sender.send(buf);
243+
}
244+
});
245+
246+
let mut writer = StdioBufWriter::new(writer);
247+
assert_matches!(
248+
writer,
249+
StdioBufWriter::BlockBuffered(_),
250+
"StdioBufWriter should be block-buffered when created from a file that is not a terminal"
251+
);
252+
253+
writer.write_all(b"line 1\n").expect("write failed");
254+
writer.write_all(b"line 2\n").expect("write failed");
255+
writer.write_all(b"line 3\n").expect("write failed");
256+
assert_matches!(receiver.try_recv(), Err(TryRecvError::Empty));
257+
258+
writer.flush().expect("flush failed");
259+
assert_eq!(receiver.recv().expect("recv failed"), b"line 1\nline 2\nline 3\n");
260+
}

library/std/src/os/fd/owned.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,13 @@ impl<'a> AsFd for io::StdinLock<'a> {
489489
}
490490
}
491491

492+
impl AsFd for io::StdinRaw {
493+
#[inline]
494+
fn as_fd(&self) -> BorrowedFd<'_> {
495+
unsafe { BorrowedFd::borrow_raw(0) }
496+
}
497+
}
498+
492499
#[stable(feature = "io_safety", since = "1.63.0")]
493500
impl AsFd for io::Stdout {
494501
#[inline]
@@ -506,6 +513,13 @@ impl<'a> AsFd for io::StdoutLock<'a> {
506513
}
507514
}
508515

516+
impl AsFd for io::StdoutRaw {
517+
#[inline]
518+
fn as_fd(&self) -> BorrowedFd<'_> {
519+
unsafe { BorrowedFd::borrow_raw(1) }
520+
}
521+
}
522+
509523
#[stable(feature = "io_safety", since = "1.63.0")]
510524
impl AsFd for io::Stderr {
511525
#[inline]
@@ -523,6 +537,13 @@ impl<'a> AsFd for io::StderrLock<'a> {
523537
}
524538
}
525539

540+
impl AsFd for io::StderrRaw {
541+
#[inline]
542+
fn as_fd(&self) -> BorrowedFd<'_> {
543+
unsafe { BorrowedFd::borrow_raw(2) }
544+
}
545+
}
546+
526547
#[stable(feature = "anonymous_pipe", since = "1.87.0")]
527548
#[cfg(not(target_os = "trusty"))]
528549
impl AsFd for io::PipeReader {

library/std/src/os/windows/io/handle.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,13 @@ impl<'a> AsHandle for crate::io::StdinLock<'a> {
562562
}
563563
}
564564

565+
impl AsHandle for crate::io::StdinRaw {
566+
#[inline]
567+
fn as_handle(&self) -> BorrowedHandle<'_> {
568+
unsafe { BorrowedHandle::borrow_raw(self.as_raw_handle()) }
569+
}
570+
}
571+
565572
#[stable(feature = "io_safety", since = "1.63.0")]
566573
impl AsHandle for crate::io::Stdout {
567574
#[inline]
@@ -578,6 +585,13 @@ impl<'a> AsHandle for crate::io::StdoutLock<'a> {
578585
}
579586
}
580587

588+
impl AsHandle for crate::io::StdoutRaw {
589+
#[inline]
590+
fn as_handle(&self) -> BorrowedHandle<'_> {
591+
unsafe { BorrowedHandle::borrow_raw(self.as_raw_handle()) }
592+
}
593+
}
594+
581595
#[stable(feature = "io_safety", since = "1.63.0")]
582596
impl AsHandle for crate::io::Stderr {
583597
#[inline]
@@ -594,6 +608,13 @@ impl<'a> AsHandle for crate::io::StderrLock<'a> {
594608
}
595609
}
596610

611+
impl AsHandle for crate::io::StderrRaw {
612+
#[inline]
613+
fn as_handle(&self) -> BorrowedHandle<'_> {
614+
unsafe { BorrowedHandle::borrow_raw(self.as_raw_handle()) }
615+
}
616+
}
617+
597618
#[stable(feature = "io_safety", since = "1.63.0")]
598619
impl AsHandle for crate::process::ChildStdin {
599620
#[inline]

0 commit comments

Comments
 (0)