| // Copyright 2019 Intel Corporation. All Rights Reserved. |
| // |
| // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| // |
| // Copyright 2017 The Chromium OS Authors. All rights reserved. |
| // |
| // SPDX-License-Identifier: BSD-3-Clause |
| |
| //! Trait for working with [`termios`](http://man7.org/linux/man-pages/man3/termios.3.html). |
| |
| use std::io::StdinLock; |
| use std::mem::zeroed; |
| use std::os::unix::io::RawFd; |
| |
| use libc::{ |
| c_int, fcntl, isatty, read, tcgetattr, tcsetattr, termios, ECHO, F_GETFL, F_SETFL, ICANON, |
| ISIG, O_NONBLOCK, STDIN_FILENO, TCSANOW, |
| }; |
| |
| use crate::errno::{errno_result, Result}; |
| |
| fn modify_mode<F: FnOnce(&mut termios)>(fd: RawFd, f: F) -> Result<()> { |
| // SAFETY: Safe because we check the return value of isatty. |
| if unsafe { isatty(fd) } != 1 { |
| return Ok(()); |
| } |
| |
| // SAFETY: The following pair are safe because termios gets totally overwritten by tcgetattr |
| // and we check the return result. |
| let mut termios: termios = unsafe { zeroed() }; |
| // SAFETY: The parameter is valid and we check the result. |
| let ret = unsafe { tcgetattr(fd, &mut termios as *mut _) }; |
| if ret < 0 { |
| return errno_result(); |
| } |
| let mut new_termios = termios; |
| f(&mut new_termios); |
| // SAFETY: Safe because the syscall will only read the extent of termios and we check the |
| // return result. |
| let ret = unsafe { tcsetattr(fd, TCSANOW, &new_termios as *const _) }; |
| if ret < 0 { |
| return errno_result(); |
| } |
| |
| Ok(()) |
| } |
| |
| fn get_flags(fd: RawFd) -> Result<c_int> { |
| // SAFETY: Safe because no third parameter is expected and we check the return result. |
| let ret = unsafe { fcntl(fd, F_GETFL) }; |
| if ret < 0 { |
| return errno_result(); |
| } |
| Ok(ret) |
| } |
| |
| fn set_flags(fd: RawFd, flags: c_int) -> Result<()> { |
| // SAFETY: Safe because we supply the third parameter and we check the return result. |
| let ret = unsafe { fcntl(fd, F_SETFL, flags) }; |
| if ret < 0 { |
| return errno_result(); |
| } |
| Ok(()) |
| } |
| |
| /// Trait for file descriptors that are TTYs, according to |
| /// [`isatty`](http://man7.org/linux/man-pages/man3/isatty.3.html). |
| /// |
| /// # Safety |
| /// |
| /// This is marked unsafe because the implementation must ensure that the returned |
| /// RawFd is a valid fd and that the lifetime of the returned fd is at least that |
| /// of the trait object. |
| pub unsafe trait Terminal { |
| /// Get the file descriptor of the TTY. |
| fn tty_fd(&self) -> RawFd; |
| |
| /// Set this terminal to canonical mode (`ICANON | ECHO | ISIG`). |
| /// |
| /// Enable canonical mode with `ISIG` that generates signal when receiving |
| /// any of the characters INTR, QUIT, SUSP, or DSUSP, and with `ECHO` that echo |
| /// the input characters. Refer to |
| /// [`termios`](http://man7.org/linux/man-pages/man3/termios.3.html). |
| fn set_canon_mode(&self) -> Result<()> { |
| modify_mode(self.tty_fd(), |t| t.c_lflag |= ICANON | ECHO | ISIG) |
| } |
| |
| /// Set this terminal to raw mode. |
| /// |
| /// Unset the canonical mode with (`!(ICANON | ECHO | ISIG)`) which means |
| /// input is available character by character, echoing is disabled and special |
| /// signal of receiving characters INTR, QUIT, SUSP, or DSUSP is disabled. |
| fn set_raw_mode(&self) -> Result<()> { |
| modify_mode(self.tty_fd(), |t| t.c_lflag &= !(ICANON | ECHO | ISIG)) |
| } |
| |
| /// Set this terminal to non-blocking mode. |
| /// |
| /// If `non_block` is `true`, then `read_raw` will not block. |
| /// If `non_block` is `false`, then `read_raw` may block if |
| /// there is nothing to read. |
| fn set_non_block(&self, non_block: bool) -> Result<()> { |
| let old_flags = get_flags(self.tty_fd())?; |
| let new_flags = if non_block { |
| old_flags | O_NONBLOCK |
| } else { |
| old_flags & !O_NONBLOCK |
| }; |
| if new_flags != old_flags { |
| set_flags(self.tty_fd(), new_flags)? |
| } |
| Ok(()) |
| } |
| |
| /// Read from a [`Terminal`](trait.Terminal.html). |
| /// |
| /// Read up to `out.len()` bytes from this terminal without any buffering. |
| /// This may block, depending on if non-blocking was enabled with `set_non_block` |
| /// or if there are any bytes to read. |
| /// If there is at least one byte that is readable, this will not block. |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// extern crate vmm_sys_util; |
| /// # use std::io; |
| /// # use std::os::unix::io::RawFd; |
| /// use vmm_sys_util::terminal::Terminal; |
| /// |
| /// let stdin_handle = io::stdin(); |
| /// let stdin = stdin_handle.lock(); |
| /// assert!(stdin.set_non_block(true).is_ok()); |
| /// |
| /// let mut out = [0u8; 0]; |
| /// assert_eq!(stdin.read_raw(&mut out[..]).unwrap(), 0); |
| /// ``` |
| fn read_raw(&self, out: &mut [u8]) -> Result<usize> { |
| // SAFETY: Safe because read will only modify the pointer up to the length we give it and |
| // we check the return result. |
| let ret = unsafe { read(self.tty_fd(), out.as_mut_ptr() as *mut _, out.len()) }; |
| if ret < 0 { |
| return errno_result(); |
| } |
| |
| Ok(ret as usize) |
| } |
| } |
| |
| // SAFETY: Safe because we return a genuine terminal fd that never changes and shares our lifetime. |
| unsafe impl<'a> Terminal for StdinLock<'a> { |
| fn tty_fd(&self) -> RawFd { |
| STDIN_FILENO |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| #![allow(clippy::undocumented_unsafe_blocks)] |
| use super::*; |
| use std::fs::File; |
| use std::io; |
| use std::os::unix::io::AsRawFd; |
| use std::path::Path; |
| |
| unsafe impl Terminal for File { |
| fn tty_fd(&self) -> RawFd { |
| self.as_raw_fd() |
| } |
| } |
| |
| #[test] |
| fn test_a_tty() { |
| let stdin_handle = io::stdin(); |
| let stdin = stdin_handle.lock(); |
| |
| assert!(stdin.set_canon_mode().is_ok()); |
| assert!(stdin.set_raw_mode().is_ok()); |
| assert!(stdin.set_raw_mode().is_ok()); |
| assert!(stdin.set_canon_mode().is_ok()); |
| assert!(stdin.set_non_block(true).is_ok()); |
| let mut out = [0u8; 0]; |
| assert!(stdin.read_raw(&mut out[..]).is_ok()); |
| } |
| |
| #[test] |
| fn test_a_non_tty() { |
| let file = File::open(Path::new("/dev/zero")).unwrap(); |
| assert!(file.set_canon_mode().is_ok()); |
| } |
| } |