Skip to content

Commit 40e66bc

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

File tree

2 files changed

+212
-12
lines changed

2 files changed

+212
-12
lines changed

library/std/src/io/stdio.rs

Lines changed: 106 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ 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
};
14+
use crate::os::fd::{AsFd, BorrowedFd};
1315
use crate::panic::{RefUnwindSafe, UnwindSafe};
1416
use crate::sync::atomic::{Atomic, AtomicBool, Ordering};
1517
use crate::sync::{Arc, Mutex, MutexGuard, OnceLock, ReentrantLock, ReentrantLockGuard};
@@ -168,6 +170,13 @@ impl Write for StdoutRaw {
168170
}
169171
}
170172

173+
impl AsFd for StdoutRaw {
174+
#[inline]
175+
fn as_fd(&self) -> BorrowedFd<'_> {
176+
unsafe { BorrowedFd::borrow_raw(1) }
177+
}
178+
}
179+
171180
impl Write for StderrRaw {
172181
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
173182
handle_ebadf(self.0.write(buf), || Ok(buf.len()))
@@ -200,6 +209,13 @@ impl Write for StderrRaw {
200209
}
201210
}
202211

212+
impl AsFd for StderrRaw {
213+
#[inline]
214+
fn as_fd(&self) -> BorrowedFd<'_> {
215+
unsafe { BorrowedFd::borrow_raw(2) }
216+
}
217+
}
218+
203219
fn handle_ebadf<T>(r: io::Result<T>, default: impl FnOnce() -> io::Result<T>) -> io::Result<T> {
204220
match r {
205221
Err(ref e) if stdio::is_ebadf(e) => default(),
@@ -576,6 +592,76 @@ impl fmt::Debug for StdinLock<'_> {
576592
}
577593
}
578594

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

615700
/// A locked reference to the [`Stdout`] handle.
@@ -638,10 +723,10 @@ pub struct Stdout {
638723
#[must_use = "if unused stdout will immediately unlock"]
639724
#[stable(feature = "rust1", since = "1.0.0")]
640725
pub struct StdoutLock<'a> {
641-
inner: ReentrantLockGuard<'a, RefCell<LineWriter<StdoutRaw>>>,
726+
inner: ReentrantLockGuard<'a, RefCell<StdioBufWriter<StdoutRaw>>>,
642727
}
643728

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

646731
/// Constructs a new handle to the standard output of the current process.
647732
///
@@ -716,7 +801,7 @@ static STDOUT: OnceLock<ReentrantLock<RefCell<LineWriter<StdoutRaw>>>> = OnceLoc
716801
pub fn stdout() -> Stdout {
717802
Stdout {
718803
inner: STDOUT
719-
.get_or_init(|| ReentrantLock::new(RefCell::new(LineWriter::new(stdout_raw())))),
804+
.get_or_init(|| ReentrantLock::new(RefCell::new(StdioBufWriter::new(stdout_raw())))),
720805
}
721806
}
722807

@@ -727,7 +812,7 @@ pub fn cleanup() {
727812
let mut initialized = false;
728813
let stdout = STDOUT.get_or_init(|| {
729814
initialized = true;
730-
ReentrantLock::new(RefCell::new(LineWriter::with_capacity(0, stdout_raw())))
815+
ReentrantLock::new(RefCell::new(StdioBufWriter::with_capacity(0, stdout_raw())))
731816
});
732817

733818
if !initialized {
@@ -736,7 +821,7 @@ pub fn cleanup() {
736821
// might have leaked a StdoutLock, which would
737822
// otherwise cause a deadlock here.
738823
if let Some(lock) = stdout.try_lock() {
739-
*lock.borrow_mut() = LineWriter::with_capacity(0, stdout_raw());
824+
*lock.borrow_mut() = StdioBufWriter::with_capacity(0, stdout_raw());
740825
}
741826
}
742827
}
@@ -1262,7 +1347,17 @@ macro_rules! impl_is_terminal {
12621347
)*}
12631348
}
12641349

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

12671362
#[unstable(
12681363
feature = "print_internals",

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

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

0 commit comments

Comments
 (0)