From b645bc990f1f2101781f4cf1571fc1e334dd35c1 Mon Sep 17 00:00:00 2001 From: Zack Weinberg Date: Fri, 17 Nov 2017 14:12:41 -0500 Subject: [PATCH 1/5] Partial implementation of `close` for stdin/out/err (#40032) This is only the low-level code required for Unix. --- src/libstd/sys/unix/fd.rs | 24 ++++++++++++++++++ src/libstd/sys/unix/stdio.rs | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/libstd/sys/unix/fd.rs b/src/libstd/sys/unix/fd.rs index 5dafc3251e755..d04a0eaf0bbe9 100644 --- a/src/libstd/sys/unix/fd.rs +++ b/src/libstd/sys/unix/fd.rs @@ -243,6 +243,30 @@ impl FileDesc { } cvt(unsafe { libc::fcntl(fd, libc::F_DUPFD, 0) }).and_then(make_filedesc) } + + /// Replace the open file referred to by `self` with the open file + /// referred to by `other`. This does not change the file + /// descriptor _number_ within self; rather, it changes what open + /// file that number refers to. + /// + /// On success, the file formerly referred to by `self` is + /// transferred to a newly-allocated file descriptor and returned. + /// On failure, the file referred to by `self` is unchanged. + /// Regardless of success or failure, `other` is consumed, which + /// means the file descriptor formerly held by `other` will be + /// closed. + /// + /// The file underlying `self` is replaced atomically, but the + /// _overall_ operation is not atomic; concurrent threads that snoop on + /// the set of valid file descriptors (which they shouldn't) can observe + /// intermediate states. + pub fn replace(&mut self, other: FileDesc) -> io::Result { + let previous = self.duplicate()?; + let fd = cvt(unsafe { libc::dup2(other.fd, self.fd) })?; + + assert!(fd == self.fd); + Ok(previous) + } } impl<'a> Read for &'a FileDesc { diff --git a/src/libstd/sys/unix/stdio.rs b/src/libstd/sys/unix/stdio.rs index e9b3d4affc7dd..5c95eccf85fdf 100644 --- a/src/libstd/sys/unix/stdio.rs +++ b/src/libstd/sys/unix/stdio.rs @@ -11,11 +11,25 @@ use io; use libc; use sys::fd::FileDesc; +use sys::fs::{File, OpenOptions}; +use ffi::CStr; pub struct Stdin(()); pub struct Stdout(()); pub struct Stderr(()); +// FIXME: This duplicates code from process_common.rs. +fn open_null_device (readable: bool) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(readable); + opts.write(!readable); + let path = unsafe { + CStr::from_ptr("/dev/null\0".as_ptr() as *const _) + }; + let fd = File::open_c(&path, &opts)?; + Ok(fd.into_fd()) +} + impl Stdin { pub fn new() -> io::Result { Ok(Stdin(())) } @@ -25,6 +39,21 @@ impl Stdin { fd.into_raw(); ret } + + pub fn close() -> io::Result<()> { + // To close stdin, what we actually do is change its well-known + // file descriptor number to refer to a file open on the null + // device. This protects against code (perhaps in third-party + // libraries) that assumes STDIN_FILENO is always open and always + // refers to the same thing as stdin. + let mut fd = FileDesc::new(libc::STDIN_FILENO); + // If this step succeeds, the "previous" file descriptor returned + // by `fd.replace` is dropped and thus closed. + fd.replace(open_null_device(true)?)?; + // Don't close STDIN_FILENO itself, though! + fd.into_raw(); + Ok(()) + } } impl Stdout { @@ -40,6 +69,15 @@ impl Stdout { pub fn flush(&self) -> io::Result<()> { Ok(()) } + + pub fn close() -> io::Result<()> { + // See commentary for Stdin::close. + + let mut fd = FileDesc::new(libc::STDOUT_FILENO); + fd.replace(open_null_device(false)?)?; + fd.into_raw(); + Ok(()) + } } impl Stderr { @@ -55,6 +93,15 @@ impl Stderr { pub fn flush(&self) -> io::Result<()> { Ok(()) } + + pub fn close() -> io::Result<()> { + // See commentary for Stdin::close. + + let mut fd = FileDesc::new(libc::STDERR_FILENO); + fd.replace(open_null_device(false)?)?; + fd.into_raw(); + Ok(()) + } } // FIXME: right now this raw stderr handle is used in a few places because From 1188c1a8f5c5ed1f7fc12bfd298693ee900b0ecb Mon Sep 17 00:00:00 2001 From: Zack Weinberg Date: Sat, 2 Dec 2017 12:50:07 -0500 Subject: [PATCH 2/5] Address review comments on low-level Unix code. * FileDesc::replace() no longer returns the old file; call duplicate() yourself if you need it. * Make sure STD{IN,OUT,ERR}_FILENO don't get closed on errors, using ManuallyDrop. * Correct signatures of close methods. --- src/libstd/sys/unix/fd.rs | 22 +++++++++++----------- src/libstd/sys/unix/stdio.rs | 33 ++++++++++++++++----------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/libstd/sys/unix/fd.rs b/src/libstd/sys/unix/fd.rs index d04a0eaf0bbe9..08761c57cc8dd 100644 --- a/src/libstd/sys/unix/fd.rs +++ b/src/libstd/sys/unix/fd.rs @@ -244,14 +244,16 @@ impl FileDesc { cvt(unsafe { libc::fcntl(fd, libc::F_DUPFD, 0) }).and_then(make_filedesc) } - /// Replace the open file referred to by `self` with the open file - /// referred to by `other`. This does not change the file - /// descriptor _number_ within self; rather, it changes what open - /// file that number refers to. + /// Atomically replace the open file referred to by `self` with + /// the open file referred to by `other`. This does not change + /// the file descriptor _number_ within self; rather, it changes + /// what open file that number refers to. + /// + /// On success, the file formerly referred to by `self` may have + /// been closed (if there was no other descriptor referring to + /// it); if this is undesirable, call duplicate() first. + /// On failure, `self` is unchanged. /// - /// On success, the file formerly referred to by `self` is - /// transferred to a newly-allocated file descriptor and returned. - /// On failure, the file referred to by `self` is unchanged. /// Regardless of success or failure, `other` is consumed, which /// means the file descriptor formerly held by `other` will be /// closed. @@ -260,12 +262,10 @@ impl FileDesc { /// _overall_ operation is not atomic; concurrent threads that snoop on /// the set of valid file descriptors (which they shouldn't) can observe /// intermediate states. - pub fn replace(&mut self, other: FileDesc) -> io::Result { - let previous = self.duplicate()?; + pub fn replace(&mut self, other: FileDesc) -> io::Result<()> { let fd = cvt(unsafe { libc::dup2(other.fd, self.fd) })?; - assert!(fd == self.fd); - Ok(previous) + Ok(()) } } diff --git a/src/libstd/sys/unix/stdio.rs b/src/libstd/sys/unix/stdio.rs index 5c95eccf85fdf..b390d0e9711f4 100644 --- a/src/libstd/sys/unix/stdio.rs +++ b/src/libstd/sys/unix/stdio.rs @@ -10,6 +10,7 @@ use io; use libc; +use mem::ManuallyDrop; use sys::fd::FileDesc; use sys::fs::{File, OpenOptions}; use ffi::CStr; @@ -40,18 +41,18 @@ impl Stdin { ret } - pub fn close() -> io::Result<()> { - // To close stdin, what we actually do is change its well-known - // file descriptor number to refer to a file open on the null - // device. This protects against code (perhaps in third-party - // libraries) that assumes STDIN_FILENO is always open and always - // refers to the same thing as stdin. - let mut fd = FileDesc::new(libc::STDIN_FILENO); - // If this step succeeds, the "previous" file descriptor returned - // by `fd.replace` is dropped and thus closed. + pub fn close(&mut self) -> io::Result<()> { + // To close stdin, atomically replace the file underlying + // STDIN_FILENO with the null device. This protects against + // code (perhaps in third-party libraries) that assumes + // STDIN_FILENO is always open and always refers to the same + // thing as stdin. + // + // This function does not "drain" any not-yet-read input. + + // STDIN_FILENO itself should never actually be closed. + let mut fd = ManuallyDrop::new(FileDesc::new(libc::STDIN_FILENO)); fd.replace(open_null_device(true)?)?; - // Don't close STDIN_FILENO itself, though! - fd.into_raw(); Ok(()) } } @@ -70,12 +71,11 @@ impl Stdout { Ok(()) } - pub fn close() -> io::Result<()> { + pub fn close(&mut self) -> io::Result<()> { // See commentary for Stdin::close. - let mut fd = FileDesc::new(libc::STDOUT_FILENO); + let mut fd = ManuallyDrop::new(FileDesc::new(libc::STDOUT_FILENO)); fd.replace(open_null_device(false)?)?; - fd.into_raw(); Ok(()) } } @@ -94,12 +94,11 @@ impl Stderr { Ok(()) } - pub fn close() -> io::Result<()> { + pub fn close(&mut self) -> io::Result<()> { // See commentary for Stdin::close. - let mut fd = FileDesc::new(libc::STDERR_FILENO); + let mut fd = ManuallyDrop::new(FileDesc::new(libc::STDERR_FILENO)); fd.replace(open_null_device(false)?)?; - fd.into_raw(); Ok(()) } } From 9f050f94dd45401c6800f2f9acd24f171200f0a3 Mon Sep 17 00:00:00 2001 From: Zack Weinberg Date: Sat, 2 Dec 2017 12:53:03 -0500 Subject: [PATCH 3/5] Add public close methods for io::std{in,out,err} and a test. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close methods are available on both Std{in,out,err} and Std{in,out,err}Lock, because closing these files should be serialized against reads/writes in other threads. They are *not* available on Std{in,out,err}Raw because that layer of indirection seems to be to do with the set_print/set_panic mechanism, which *doesn't* operate by changing what fds 0/1/2 refer to. That maybe ought to get cleaned up, but Not In This Patchsetâ„¢. The test is known to succeed on Unix. --- src/libstd/io/stdio.rs | 142 ++++++++++++++++++++++++- src/test/run-pass/close-std-streams.rs | 129 ++++++++++++++++++++++ 2 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 src/test/run-pass/close-std-streams.rs diff --git a/src/libstd/io/stdio.rs b/src/libstd/io/stdio.rs index 831688bb73d1c..889d4a2d6061e 100644 --- a/src/libstd/io/stdio.rs +++ b/src/libstd/io/stdio.rs @@ -260,7 +260,7 @@ impl Stdin { /// println!("{} bytes read", n); /// println!("{}", input); /// } - /// Err(error) => println!("error: {}", error), + /// Err(error) => eprintln!("error: {}", error), /// } /// ``` /// @@ -274,6 +274,50 @@ impl Stdin { pub fn read_line(&self, buf: &mut String) -> io::Result { self.lock().read_line(buf) } + + /// Lock this handle and then close it. + /// + /// "Closing" standard input actually replaces it with a file open + /// on the null device. Thus, after `stdin().close()`, both stdin + /// and `libc::STDIN_FILENO` can still be used, but will read as empty. + /// However, the original file has indeed been closed; for instance, + /// if standard input is a pipe, anyone still writing to it will receive + /// a "broken pipe" notification. + /// + /// # Examples + /// + /// ```no_run + /// use std::io; + /// + /// let mut input = String::new(); + /// + /// match io::stdin().read_line(&mut input) { + /// Ok(n) => { + /// println!("{} bytes read", n); + /// println!("{}", input); + /// } + /// Err(error) => eprintln!("read error: {}", error), + /// } + /// + /// if let Err(error) = io::stdin().close() { + /// eprintln!("close error: {}", error); + /// } + /// + /// match io::stdin().read_line(&mut input) { + /// Ok(n) => { + /// println!("{} bytes read", n); + /// println!("{}", input); + /// } + /// Err(error) => eprintln!("read error: {}", error), + /// } + /// ``` + /// + /// If this program is run with two or more lines of input, it will + /// print only the first line. + #[unstable(feature = "close_std_streams", issue = "40032")] + pub fn close(&mut self) -> io::Result<()> { + self.lock().close() + } } #[stable(feature = "std_debug", since = "1.16.0")] @@ -303,6 +347,20 @@ impl Read for Stdin { } } +impl<'a> StdinLock<'a> { + /// Close this file handle. + /// + /// For detailed semantics of this method, see the documentation on + /// [`Stdin::close`]. + /// + /// [`Stdin::close`]: struct.Stdin.html#method.close + #[unstable(feature = "close_std_streams", issue = "40032")] + pub fn close(&mut self) -> io::Result<()> { + // Do this regardless of whether inner is real. + stdio::Stdin::new()?.close() + } +} + #[stable(feature = "rust1", since = "1.0.0")] impl<'a> Read for StdinLock<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { @@ -435,6 +493,32 @@ impl Stdout { pub fn lock(&self) -> StdoutLock { StdoutLock { inner: self.inner.lock().unwrap_or_else(|e| e.into_inner()) } } + + /// Lock this handle and then close it. + /// + /// "Closing" standard output actually replaces it with a file open + /// on the null device. Thus, after `stdout().close()`, both stdout + /// and `libc::STDOUT_FILENO` can still be used, but will discard + /// everything written to them. However, the original file has + /// indeed been closed; for instance, if standard output is a pipe, + /// whoever is reading from it will receive an end-of-file notification. + /// + /// # Examples + /// + /// ```no_run + /// use std::io; + /// + /// println!("this line is printed"); + /// if let Err(error) = io::stdout().close() { + /// eprintln!("close error: {}", error); + /// } else { + /// println!("this line is discarded"); + /// } + /// ``` + #[unstable(feature = "close_std_streams", issue = "40032")] + pub fn close(&mut self) -> io::Result<()> { + self.lock().close() + } } #[stable(feature = "std_debug", since = "1.16.0")] @@ -459,6 +543,21 @@ impl Write for Stdout { self.lock().write_fmt(args) } } + +impl<'a> StdoutLock<'a> { + /// Close this file handle. + /// + /// For detailed semantics of this method, see the documentation on + /// [`Stdout::close`]. + /// + /// [`Stdout::close`]: struct.Stdout.html#method.close + #[unstable(feature = "close_std_streams", issue = "40032")] + pub fn close(&mut self) -> io::Result<()> { + // Do this regardless of whether inner is real. + stdio::Stdout::new()?.close() + } +} + #[stable(feature = "rust1", since = "1.0.0")] impl<'a> Write for StdoutLock<'a> { fn write(&mut self, buf: &[u8]) -> io::Result { @@ -570,6 +669,32 @@ impl Stderr { pub fn lock(&self) -> StderrLock { StderrLock { inner: self.inner.lock().unwrap_or_else(|e| e.into_inner()) } } + + /// Lock this handle and then close it. + /// + /// "Closing" standard error actually replaces it with a file open + /// on the null device. Thus, after `stderr().close()`, both stderr + /// and `libc::STDERR_FILENO` can still be used, but will discard + /// everything written to them. However, the original file has + /// indeed been closed; for instance, if standard error is a pipe, + /// whoever is reading from it will receive an end-of-file notification. + /// + /// # Examples + /// + /// ```no_run + /// use std::io; + /// + /// eprintln!("this line is printed"); + /// if let Err(error) = io::stdout().close() { + /// eprintln!("close error: {}", error); + /// } else { + /// eprintln!("this line is discarded"); + /// } + /// ``` + #[unstable(feature = "close_std_streams", issue = "40032")] + pub fn close(&mut self) -> io::Result<()> { + self.lock().close() + } } #[stable(feature = "std_debug", since = "1.16.0")] @@ -594,6 +719,21 @@ impl Write for Stderr { self.lock().write_fmt(args) } } + +impl<'a> StderrLock<'a> { + /// Close this file handle. + /// + /// For detailed semantics of this method, see the documentation on + /// [`Stderr::close`]. + /// + /// [`Stderr::close`]: struct.Stderr.html#method.close + #[unstable(feature = "close_std_streams", issue = "40032")] + pub fn close(&mut self) -> io::Result<()> { + // Do this regardless of whether inner is real. + stdio::Stderr::new()?.close() + } +} + #[stable(feature = "rust1", since = "1.0.0")] impl<'a> Write for StderrLock<'a> { fn write(&mut self, buf: &[u8]) -> io::Result { diff --git a/src/test/run-pass/close-std-streams.rs b/src/test/run-pass/close-std-streams.rs new file mode 100644 index 0000000000000..e0df598537609 --- /dev/null +++ b/src/test/run-pass/close-std-streams.rs @@ -0,0 +1,129 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// ignore-emscripten spawning processes is not supported + +#![feature(close_std_streams)] + +use std::{env, io, process}; +use std::io::{Read, Write}; + +fn expect_read(stream: &mut Read, msg: &[u8]) { + assert!(msg.len() < 128); + + let mut buf = [0u8; 128]; + match stream.read(&mut buf) { + Ok(n) => { + assert_eq!(n, msg.len()); + if n > 0 { + assert_eq!(&buf[..n], msg); + } + }, + Err(e) => panic!("read error: {}", e) + } +} + +// Note: order of operations is critical in this program. +// Parent and child are synchronized by which streams are closed when. + +fn child_locked() { + let stdin_u = io::stdin(); + let stdout_u = io::stdout(); + let stderr_u = io::stderr(); + let mut stdin = stdin_u.lock(); + let mut stdout = stdout_u.lock(); + let mut stderr = stderr_u.lock(); + + expect_read(&mut stdin, b"in1\n"); + stdin.close().unwrap(); + + stdout.write(b"ou1\n").unwrap(); + stdout.close().unwrap(); + stdout.write(b"ou2\n").unwrap(); + + expect_read(&mut stdin, b""); + + // stderr is tested last, because we will lose the ability to emit + // panic messages when we close stderr. + stderr.write(b"er1\n").unwrap(); + stderr.close().unwrap(); + stderr.write(b"er2\n").unwrap(); +} + +fn child_unlocked() { + expect_read(&mut io::stdin(), b"in1\n"); + io::stdin().close().unwrap(); + + io::stdout().write(b"ou1\n").unwrap(); + io::stdout().close().unwrap(); + io::stdout().write(b"ou2\n").unwrap(); + + expect_read(&mut io::stdin(), b""); + + // stderr is tested last, because we will lose the ability to emit + // panic messages when we close stderr. + io::stderr().write(b"er1\n").unwrap(); + io::stderr().close().unwrap(); + io::stderr().write(b"er2\n").unwrap(); +} + +fn parent(arg: &'static str) { + let this = env::args().next().unwrap(); + let mut child = process::Command::new(this) + .arg(arg) + .stdin(process::Stdio::piped()) + .stdout(process::Stdio::piped()) + .stderr(process::Stdio::piped()) + .spawn() + .unwrap(); + + let mut c_stdin = child.stdin.take().unwrap(); + let mut c_stdout = child.stdout.take().unwrap(); + let mut c_stderr = child.stderr.take().unwrap(); + + // this will be received by the child + c_stdin.write(b"in1\n").unwrap(); + + // reading this also synchronizes with the child closing its stdin + expect_read(&mut c_stdout, b"ou1\n"); + + // this should signal a broken pipe + match c_stdin.write(b"in2\n") { + Ok(_) => panic!("second write to child should not have succeeded"), + Err(e) => { + if e.kind() != io::ErrorKind::BrokenPipe { + panic!("second write to child failed the wrong way: {}", e) + } + } + } + + expect_read(&mut c_stdout, b""); + expect_read(&mut c_stderr, b"er1\n"); + expect_read(&mut c_stderr, b""); + + let status = child.wait().unwrap(); + assert!(status.success()); +} + +fn main() { + let n = env::args().count(); + if n == 1 { + parent("L"); + parent("U"); + } else if n == 2 { + match env::args().nth(1).unwrap().as_ref() { + "L" => child_locked(), + "U" => child_unlocked(), + s => panic!("child selector {} not recognized", s) + } + } else { + panic!("wrong number of arguments - {}", n) + } +} From 9d63a4a9ea6f8d112e36ee847837317f8ab7e43f Mon Sep 17 00:00:00 2001 From: Zack Weinberg Date: Sat, 2 Dec 2017 12:58:54 -0500 Subject: [PATCH 4/5] Low-level std-streams-closing methods for Redox. Very nearly the same as the code for Unix. (What's with the extra buffer argument to dup2, though?) Note: the duplicate code for opening the null device should get refactored, but we need to decide where to put it first, and that probably needs its own feature discussion. --- src/libstd/sys/redox/fd.rs | 6 ++++++ src/libstd/sys/redox/stdio.rs | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/libstd/sys/redox/fd.rs b/src/libstd/sys/redox/fd.rs index ba7bbdc657fcf..99f0d26f11cfa 100644 --- a/src/libstd/sys/redox/fd.rs +++ b/src/libstd/sys/redox/fd.rs @@ -71,6 +71,12 @@ impl FileDesc { } cvt(syscall::fcntl(self.fd, syscall::F_SETFL, flags)).and(Ok(())) } + + pub fn replace(&mut self, other: FileDesc) -> io::Result<()> { + let fd = cvt(syscall::dup2(other.fd, self.fd, &[]))?; + assert!(fd == self.fd); + Ok(()) + } } impl<'a> Read for &'a FileDesc { diff --git a/src/libstd/sys/redox/stdio.rs b/src/libstd/sys/redox/stdio.rs index 3abb094ac34e3..e6848c93411bc 100644 --- a/src/libstd/sys/redox/stdio.rs +++ b/src/libstd/sys/redox/stdio.rs @@ -9,6 +9,7 @@ // except according to those terms. use io; +use mem::ManuallyDrop; use sys::{cvt, syscall}; use sys::fd::FileDesc; @@ -16,6 +17,15 @@ pub struct Stdin(()); pub struct Stdout(()); pub struct Stderr(()); +// FIXME: This duplicates code from process.rs. +fn open_null_device (readable: bool) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(readable); + opts.write(!readable); + let fd = File::open(Path::new("null:"), &opts)?; + Ok(fd.into_fd()) +} + impl Stdin { pub fn new() -> io::Result { Ok(Stdin(())) } @@ -25,6 +35,20 @@ impl Stdin { fd.into_raw(); ret } + + pub fn close(&mut self) -> io::Result<()> { + // To close stdin, atomically replace the file underlying fd 0 + // with the null device. This protects against code (perhaps + // in third-party libraries) that assumes fd 0 is always open + // and always refers to the same thing as stdin. + // + // This function does not "drain" any not-yet-read input. + + // fd 0 itself should never actually be closed. + let mut fd = ManuallyDrop::new(FileDesc::new(0)); + fd.replace(open_null_device(true)?)?; + Ok(()) + } } impl Stdout { @@ -40,6 +64,14 @@ impl Stdout { pub fn flush(&self) -> io::Result<()> { cvt(syscall::fsync(1)).and(Ok(())) } + + pub fn close(&mut self) -> io::Result<()> { + // See commentary for Stdin::close. + + let mut fd = ManuallyDrop::new(FileDesc::new(2)); + fd.replace(open_null_device(false)?)?; + Ok(()) + } } impl Stderr { @@ -55,6 +87,14 @@ impl Stderr { pub fn flush(&self) -> io::Result<()> { cvt(syscall::fsync(2)).and(Ok(())) } + + pub fn close(&mut self) -> io::Result<()> { + // See commentary for Stdin::close. + + let mut fd = ManuallyDrop::new(FileDesc::new(2)); + fd.replace(open_null_device(false)?)?; + Ok(()) + } } // FIXME: right now this raw stderr handle is used in a few places because From 8e137e59b1b660552816f6aeb4f14b15d07c59c5 Mon Sep 17 00:00:00 2001 From: Zack Weinberg Date: Sun, 3 Dec 2017 18:16:45 -0500 Subject: [PATCH 5/5] Add feature annotations to doctests. --- src/libstd/io/stdio.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libstd/io/stdio.rs b/src/libstd/io/stdio.rs index 889d4a2d6061e..adfb985016ee0 100644 --- a/src/libstd/io/stdio.rs +++ b/src/libstd/io/stdio.rs @@ -287,6 +287,7 @@ impl Stdin { /// # Examples /// /// ```no_run + /// # #![feature(close_std_streams)] /// use std::io; /// /// let mut input = String::new(); @@ -506,6 +507,7 @@ impl Stdout { /// # Examples /// /// ```no_run + /// # #![feature(close_std_streams)] /// use std::io; /// /// println!("this line is printed"); @@ -682,6 +684,7 @@ impl Stderr { /// # Examples /// /// ```no_run + /// # #![feature(close_std_streams)] /// use std::io; /// /// eprintln!("this line is printed");