Skip to content

Commit b360c25

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

File tree

2 files changed

+199
-12
lines changed

2 files changed

+199
-12
lines changed

library/std/src/io/stdio.rs

Lines changed: 104 additions & 11 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};
@@ -168,6 +169,20 @@ impl Write for StdoutRaw {
168169
}
169170
}
170171

172+
#[cfg(any(
173+
unix,
174+
target_os = "hermit",
175+
target_os = "trusty",
176+
target_os = "wasi",
177+
target_os = "motor",
178+
))]
179+
impl crate::os::fd::AsFd for StdoutRaw {
180+
#[inline]
181+
fn as_fd(&self) -> crate::os::fd::BorrowedFd<'_> {
182+
unsafe { crate::os::fd::BorrowedFd::borrow_raw(1) }
183+
}
184+
}
185+
171186
impl Write for StderrRaw {
172187
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
173188
handle_ebadf(self.0.write(buf), || Ok(buf.len()))
@@ -576,6 +591,76 @@ impl fmt::Debug for StdinLock<'_> {
576591
}
577592
}
578593

594+
/// A buffered writer for stdout and stderr.
595+
///
596+
/// This writer may be either [line-buffered](LineWriter) or [block-buffered](BufWriter), depending
597+
/// on whether the underlying file is a terminal or not.
598+
#[derive(Debug)]
599+
enum StdioBufWriter<W: Write> {
600+
LineBuffered(LineWriter<W>),
601+
BlockBuffered(BufWriter<W>),
602+
}
603+
604+
impl<W: Write + IsTerminal> StdioBufWriter<W> {
605+
/// Wraps a writer using the most appropriate buffering method.
606+
///
607+
/// If `w` is a terminal, then the resulting `StdioBufWriter` will be line-buffered, otherwise
608+
/// it will be block-buffered.
609+
fn new(w: W) -> Self {
610+
if w.is_terminal() {
611+
Self::LineBuffered(LineWriter::new(w))
612+
} else {
613+
Self::BlockBuffered(BufWriter::new(w))
614+
}
615+
}
616+
}
617+
618+
impl<W: Write> StdioBufWriter<W> {
619+
/// Wraps a writer using a block-buffer with the given capacity.
620+
fn with_capacity(cap: usize, w: W) -> Self {
621+
Self::BlockBuffered(BufWriter::with_capacity(cap, w))
622+
}
623+
}
624+
625+
impl<W: Write> Write for StdioBufWriter<W> {
626+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
627+
match self {
628+
Self::LineBuffered(w) => w.write(buf),
629+
Self::BlockBuffered(w) => w.write(buf),
630+
}
631+
}
632+
fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
633+
match self {
634+
Self::LineBuffered(w) => w.write_vectored(bufs),
635+
Self::BlockBuffered(w) => w.write_vectored(bufs),
636+
}
637+
}
638+
fn is_write_vectored(&self) -> bool {
639+
match self {
640+
Self::LineBuffered(w) => w.is_write_vectored(),
641+
Self::BlockBuffered(w) => w.is_write_vectored(),
642+
}
643+
}
644+
fn flush(&mut self) -> io::Result<()> {
645+
match self {
646+
Self::LineBuffered(w) => w.flush(),
647+
Self::BlockBuffered(w) => w.flush(),
648+
}
649+
}
650+
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
651+
match self {
652+
Self::LineBuffered(w) => w.write_all(buf),
653+
Self::BlockBuffered(w) => w.write_all(buf),
654+
}
655+
}
656+
fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> io::Result<()> {
657+
match self {
658+
Self::LineBuffered(w) => w.write_all_vectored(bufs),
659+
Self::BlockBuffered(w) => w.write_all_vectored(bufs),
660+
}
661+
}
662+
}
663+
579664
/// A handle to the global standard output stream of the current process.
580665
///
581666
/// Each handle shares a global buffer of data to be written to the standard
@@ -606,10 +691,9 @@ impl fmt::Debug for StdinLock<'_> {
606691
/// [`io::stdout`]: stdout
607692
#[stable(feature = "rust1", since = "1.0.0")]
608693
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>>>,
694+
// FIXME: if this is not line buffered it should flush-on-panic or some
695+
// form of flush-on-abort.
696+
inner: &'static ReentrantLock<RefCell<StdioBufWriter<StdoutRaw>>>,
613697
}
614698

615699
/// A locked reference to the [`Stdout`] handle.
@@ -638,10 +722,10 @@ pub struct Stdout {
638722
#[must_use = "if unused stdout will immediately unlock"]
639723
#[stable(feature = "rust1", since = "1.0.0")]
640724
pub struct StdoutLock<'a> {
641-
inner: ReentrantLockGuard<'a, RefCell<LineWriter<StdoutRaw>>>,
725+
inner: ReentrantLockGuard<'a, RefCell<StdioBufWriter<StdoutRaw>>>,
642726
}
643727

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

646730
/// Constructs a new handle to the standard output of the current process.
647731
///
@@ -716,7 +800,7 @@ static STDOUT: OnceLock<ReentrantLock<RefCell<LineWriter<StdoutRaw>>>> = OnceLoc
716800
pub fn stdout() -> Stdout {
717801
Stdout {
718802
inner: STDOUT
719-
.get_or_init(|| ReentrantLock::new(RefCell::new(LineWriter::new(stdout_raw())))),
803+
.get_or_init(|| ReentrantLock::new(RefCell::new(StdioBufWriter::new(stdout_raw())))),
720804
}
721805
}
722806

@@ -727,7 +811,7 @@ pub fn cleanup() {
727811
let mut initialized = false;
728812
let stdout = STDOUT.get_or_init(|| {
729813
initialized = true;
730-
ReentrantLock::new(RefCell::new(LineWriter::with_capacity(0, stdout_raw())))
814+
ReentrantLock::new(RefCell::new(StdioBufWriter::with_capacity(0, stdout_raw())))
731815
});
732816

733817
if !initialized {
@@ -736,7 +820,7 @@ pub fn cleanup() {
736820
// might have leaked a StdoutLock, which would
737821
// otherwise cause a deadlock here.
738822
if let Some(lock) = stdout.try_lock() {
739-
*lock.borrow_mut() = LineWriter::with_capacity(0, stdout_raw());
823+
*lock.borrow_mut() = StdioBufWriter::with_capacity(0, stdout_raw());
740824
}
741825
}
742826
}
@@ -1262,7 +1346,16 @@ macro_rules! impl_is_terminal {
12621346
)*}
12631347
}
12641348

1265-
impl_is_terminal!(File, Stdin, StdinLock<'_>, Stdout, StdoutLock<'_>, Stderr, StderrLock<'_>);
1349+
impl_is_terminal!(
1350+
File,
1351+
Stdin,
1352+
StdinLock<'_>,
1353+
Stdout,
1354+
StdoutLock<'_>,
1355+
StdoutRaw,
1356+
Stderr,
1357+
StderrLock<'_>,
1358+
);
12661359

12671360
#[unstable(
12681361
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+
}

0 commit comments

Comments
 (0)