diff --git a/src/libstd/io/stdio.rs b/src/libstd/io/stdio.rs index 831688bb73d1c..adfb985016ee0 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,51 @@ 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 + /// # #![feature(close_std_streams)] + /// 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 +348,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 +494,33 @@ 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 + /// # #![feature(close_std_streams)] + /// 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 +545,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 +671,33 @@ 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 + /// # #![feature(close_std_streams)] + /// 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 +722,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/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 diff --git a/src/libstd/sys/unix/fd.rs b/src/libstd/sys/unix/fd.rs index 5dafc3251e755..08761c57cc8dd 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) } + + /// 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. + /// + /// 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 fd = cvt(unsafe { libc::dup2(other.fd, self.fd) })?; + assert!(fd == self.fd); + Ok(()) + } } impl<'a> Read for &'a FileDesc { diff --git a/src/libstd/sys/unix/stdio.rs b/src/libstd/sys/unix/stdio.rs index e9b3d4affc7dd..b390d0e9711f4 100644 --- a/src/libstd/sys/unix/stdio.rs +++ b/src/libstd/sys/unix/stdio.rs @@ -10,12 +10,27 @@ use io; use libc; +use mem::ManuallyDrop; 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 +40,21 @@ impl Stdin { fd.into_raw(); ret } + + 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)?)?; + Ok(()) + } } impl Stdout { @@ -40,6 +70,14 @@ impl Stdout { pub fn flush(&self) -> io::Result<()> { Ok(()) } + + pub fn close(&mut self) -> io::Result<()> { + // See commentary for Stdin::close. + + let mut fd = ManuallyDrop::new(FileDesc::new(libc::STDOUT_FILENO)); + fd.replace(open_null_device(false)?)?; + Ok(()) + } } impl Stderr { @@ -55,6 +93,14 @@ impl Stderr { pub fn flush(&self) -> io::Result<()> { Ok(()) } + + pub fn close(&mut self) -> io::Result<()> { + // See commentary for Stdin::close. + + let mut fd = ManuallyDrop::new(FileDesc::new(libc::STDERR_FILENO)); + fd.replace(open_null_device(false)?)?; + Ok(()) + } } // FIXME: right now this raw stderr handle is used in a few places because 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) + } +}