vmm_sys_util/unix/terminal.rs
1// Copyright 2019 Intel Corporation. All Rights Reserved.
2//
3// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4//
5// Copyright 2017 The Chromium OS Authors. All rights reserved.
6//
7// SPDX-License-Identifier: BSD-3-Clause
8
9//! Trait for working with [`termios`](http://man7.org/linux/man-pages/man3/termios.3.html).
10
11use std::io::StdinLock;
12use std::mem::zeroed;
13use std::os::unix::io::RawFd;
14
15use libc::{
16 c_int, fcntl, isatty, read, tcgetattr, tcsetattr, termios, ECHO, F_GETFL, F_SETFL, ICANON,
17 ISIG, O_NONBLOCK, STDIN_FILENO, TCSANOW,
18};
19
20use crate::errno::{errno_result, Result};
21
22fn modify_mode<F: FnOnce(&mut termios)>(fd: RawFd, f: F) -> Result<()> {
23 // SAFETY: Safe because we check the return value of isatty.
24 if unsafe { isatty(fd) } != 1 {
25 return Ok(());
26 }
27
28 // SAFETY: The following pair are safe because termios gets totally overwritten by tcgetattr
29 // and we check the return result.
30 let mut termios: termios = unsafe { zeroed() };
31 // SAFETY: The parameter is valid and we check the result.
32 let ret = unsafe { tcgetattr(fd, &mut termios as *mut _) };
33 if ret < 0 {
34 return errno_result();
35 }
36 let mut new_termios = termios;
37 f(&mut new_termios);
38 // SAFETY: Safe because the syscall will only read the extent of termios and we check the
39 // return result.
40 let ret = unsafe { tcsetattr(fd, TCSANOW, &new_termios as *const _) };
41 if ret < 0 {
42 return errno_result();
43 }
44
45 Ok(())
46}
47
48fn get_flags(fd: RawFd) -> Result<c_int> {
49 // SAFETY: Safe because no third parameter is expected and we check the return result.
50 let ret = unsafe { fcntl(fd, F_GETFL) };
51 if ret < 0 {
52 return errno_result();
53 }
54 Ok(ret)
55}
56
57fn set_flags(fd: RawFd, flags: c_int) -> Result<()> {
58 // SAFETY: Safe because we supply the third parameter and we check the return result.
59 let ret = unsafe { fcntl(fd, F_SETFL, flags) };
60 if ret < 0 {
61 return errno_result();
62 }
63 Ok(())
64}
65
66/// Trait for file descriptors that are TTYs, according to
67/// [`isatty`](http://man7.org/linux/man-pages/man3/isatty.3.html).
68///
69/// # Safety
70///
71/// This is marked unsafe because the implementation must ensure that the returned
72/// RawFd is a valid fd and that the lifetime of the returned fd is at least that
73/// of the trait object.
74pub unsafe trait Terminal {
75 /// Get the file descriptor of the TTY.
76 fn tty_fd(&self) -> RawFd;
77
78 /// Set this terminal to canonical mode (`ICANON | ECHO | ISIG`).
79 ///
80 /// Enable canonical mode with `ISIG` that generates signal when receiving
81 /// any of the characters INTR, QUIT, SUSP, or DSUSP, and with `ECHO` that echo
82 /// the input characters. Refer to
83 /// [`termios`](http://man7.org/linux/man-pages/man3/termios.3.html).
84 fn set_canon_mode(&self) -> Result<()> {
85 modify_mode(self.tty_fd(), |t| t.c_lflag |= ICANON | ECHO | ISIG)
86 }
87
88 /// Set this terminal to raw mode.
89 ///
90 /// Unset the canonical mode with (`!(ICANON | ECHO | ISIG)`) which means
91 /// input is available character by character, echoing is disabled and special
92 /// signal of receiving characters INTR, QUIT, SUSP, or DSUSP is disabled.
93 fn set_raw_mode(&self) -> Result<()> {
94 modify_mode(self.tty_fd(), |t| t.c_lflag &= !(ICANON | ECHO | ISIG))
95 }
96
97 /// Set this terminal to non-blocking mode.
98 ///
99 /// If `non_block` is `true`, then `read_raw` will not block.
100 /// If `non_block` is `false`, then `read_raw` may block if
101 /// there is nothing to read.
102 fn set_non_block(&self, non_block: bool) -> Result<()> {
103 let old_flags = get_flags(self.tty_fd())?;
104 let new_flags = if non_block {
105 old_flags | O_NONBLOCK
106 } else {
107 old_flags & !O_NONBLOCK
108 };
109 if new_flags != old_flags {
110 set_flags(self.tty_fd(), new_flags)?
111 }
112 Ok(())
113 }
114
115 /// Read from a [`Terminal`](trait.Terminal.html).
116 ///
117 /// Read up to `out.len()` bytes from this terminal without any buffering.
118 /// This may block, depending on if non-blocking was enabled with `set_non_block`
119 /// or if there are any bytes to read.
120 /// If there is at least one byte that is readable, this will not block.
121 ///
122 /// # Examples
123 ///
124 /// ```
125 /// extern crate vmm_sys_util;
126 /// # use std::io;
127 /// # use std::os::unix::io::RawFd;
128 /// use vmm_sys_util::terminal::Terminal;
129 ///
130 /// let stdin_handle = io::stdin();
131 /// let stdin = stdin_handle.lock();
132 /// assert!(stdin.set_non_block(true).is_ok());
133 ///
134 /// let mut out = [0u8; 0];
135 /// assert_eq!(stdin.read_raw(&mut out[..]).unwrap(), 0);
136 /// ```
137 fn read_raw(&self, out: &mut [u8]) -> Result<usize> {
138 // SAFETY: Safe because read will only modify the pointer up to the length we give it and
139 // we check the return result.
140 let ret = unsafe { read(self.tty_fd(), out.as_mut_ptr() as *mut _, out.len()) };
141 if ret < 0 {
142 return errno_result();
143 }
144
145 Ok(ret as usize)
146 }
147}
148
149// SAFETY: Safe because we return a genuine terminal fd that never changes and shares our lifetime.
150unsafe impl<'a> Terminal for StdinLock<'a> {
151 fn tty_fd(&self) -> RawFd {
152 STDIN_FILENO
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 #![allow(clippy::undocumented_unsafe_blocks)]
159 use super::*;
160 use std::fs::File;
161 use std::io;
162 use std::os::unix::io::AsRawFd;
163 use std::path::Path;
164
165 unsafe impl Terminal for File {
166 fn tty_fd(&self) -> RawFd {
167 self.as_raw_fd()
168 }
169 }
170
171 #[test]
172 fn test_a_tty() {
173 let stdin_handle = io::stdin();
174 let stdin = stdin_handle.lock();
175
176 assert!(stdin.set_canon_mode().is_ok());
177 assert!(stdin.set_raw_mode().is_ok());
178 assert!(stdin.set_raw_mode().is_ok());
179 assert!(stdin.set_canon_mode().is_ok());
180 assert!(stdin.set_non_block(true).is_ok());
181 let mut out = [0u8; 0];
182 assert!(stdin.read_raw(&mut out[..]).is_ok());
183 }
184
185 #[test]
186 fn test_a_non_tty() {
187 let file = File::open(Path::new("/dev/zero")).unwrap();
188 assert!(file.set_canon_mode().is_ok());
189 }
190}