diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 132c6c4d4..8d463ee3d 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -110,3 +110,4 @@ utf8 = [] utmpx = ["time", "time/macros", "libc", "dns-lookup"] version-cmp = [] wide = [] +tty = [] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index abf401008..7237788c9 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -47,6 +47,8 @@ pub mod perms; pub mod pipes; #[cfg(all(target_os = "linux", feature = "proc-info"))] pub mod proc_info; +#[cfg(all(target_os = "linux", feature = "tty"))] +pub mod tty; #[cfg(all(unix, feature = "process"))] pub mod process; diff --git a/src/uucore/src/lib/features/proc_info.rs b/src/uucore/src/lib/features/proc_info.rs index 41e0144be..fc359492e 100644 --- a/src/uucore/src/lib/features/proc_info.rs +++ b/src/uucore/src/lib/features/proc_info.rs @@ -19,6 +19,8 @@ //! `snice` (TBD) //! +use crate::features::tty::Teletype; +use std::hash::Hash; use std::{ collections::{HashMap, HashSet}, fmt::{self, Display, Formatter}, @@ -28,73 +30,6 @@ use std::{ }; use walkdir::{DirEntry, WalkDir}; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum TerminalType { - Tty(u64), - TtyS(u64), - Pts(u64), -} - -impl TryFrom for TerminalType { - type Error = (); - - fn try_from(value: String) -> Result { - Self::try_from(value.as_str()) - } -} - -impl TryFrom<&str> for TerminalType { - type Error = (); - - fn try_from(value: &str) -> Result { - Self::try_from(PathBuf::from(value)) - } -} - -impl TryFrom for TerminalType { - type Error = (); - - fn try_from(value: PathBuf) -> Result { - // Three case: /dev/pts/* , /dev/ttyS**, /dev/tty** - - let mut iter = value.iter(); - // Case 1 - - // Considering this format: **/**/pts/ - if let (Some(_), Some(num)) = (iter.find(|it| *it == "pts"), iter.next()) { - return num - .to_str() - .ok_or(())? - .parse::() - .map_err(|_| ()) - .map(TerminalType::Pts); - }; - - // Considering this format: **/**/ttyS** then **/**/tty** - let path = value.to_str().ok_or(())?; - - let f = |prefix: &str| { - value - .iter() - .last()? - .to_str()? - .strip_prefix(prefix)? - .parse::() - .ok() - }; - - if path.contains("ttyS") { - // Case 2 - f("ttyS").ok_or(()).map(TerminalType::TtyS) - } else if path.contains("tty") { - // Case 3 - f("tty").ok_or(()).map(TerminalType::Tty) - } else { - Err(()) - } - } -} - /// State or process #[derive(Debug, PartialEq, Eq)] pub enum RunState { @@ -162,8 +97,16 @@ impl TryFrom for RunState { } } +impl TryFrom<&String> for RunState { + type Error = io::Error; + + fn try_from(value: &String) -> Result { + Self::try_from(value.as_str()) + } +} + /// Process ID and its information -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ProcessInformation { pub pid: usize, pub cmdline: String, @@ -177,7 +120,7 @@ pub struct ProcessInformation { cached_stat: Option>>, cached_start_time: Option, - cached_tty: Option>>, + cached_tty: Option>>, } impl ProcessInformation { @@ -252,7 +195,7 @@ impl ProcessInformation { } /// Collect information from `/proc//stat` file - fn stat(&mut self) -> Rc> { + pub fn stat(&mut self) -> Rc> { if let Some(c) = &self.cached_stat { return Rc::clone(c); } @@ -264,7 +207,7 @@ impl ProcessInformation { Rc::clone(&result) } - /// Fetch start time + /// Fetch start time from [ProcessInformation::cached_stat] /// /// - [The /proc Filesystem: Table 1-4](https://docs.kernel.org/filesystems/proc.html#id10) pub fn start_time(&mut self) -> Result { @@ -286,7 +229,7 @@ impl ProcessInformation { Ok(time) } - /// Fetch run state + /// Fetch run state from [ProcessInformation::cached_stat] /// /// - [The /proc Filesystem: Table 1-4](https://docs.kernel.org/filesystems/proc.html#id10) /// @@ -299,25 +242,38 @@ impl ProcessInformation { /// This function will scan the `/proc//fd` directory /// + /// If the process does not belong to any terminal, + /// the result will contain [TerminalType::Unknown]. + /// + /// Otherwise [TerminalType::Unknown] does not appear in the result. + /// /// # Error /// /// If scanned pid had mismatched permission, /// it will caused [std::io::ErrorKind::PermissionDenied] error. - pub fn ttys(&mut self) -> Result>, io::Error> { + pub fn ttys(&mut self) -> Result>, io::Error> { if let Some(tty) = &self.cached_tty { return Ok(Rc::clone(tty)); } let path = PathBuf::from(format!("/proc/{}/fd", self.pid)); - let result = Rc::new( - fs::read_dir(path)? - .flatten() - .filter(|it| it.path().is_symlink()) - .flat_map(|it| fs::read_link(it.path())) - .flat_map(TerminalType::try_from) - .collect::>(), - ); + let Ok(result) = fs::read_dir(path) else { + return Ok(Rc::new(HashSet::from_iter([Teletype::Unknown]))); + }; + + let mut result = result + .flatten() + .filter(|it| it.path().is_symlink()) + .flat_map(|it| fs::read_link(it.path())) + .flat_map(Teletype::try_from) + .collect::>(); + + if result.is_empty() { + result.insert(Teletype::Unknown); + } + + let result = Rc::new(result); self.cached_tty = Some(Rc::clone(&result)); @@ -335,6 +291,15 @@ impl TryFrom for ProcessInformation { } } +impl Hash for ProcessInformation { + fn hash(&self, state: &mut H) { + // Make it faster. + self.pid.hash(state); + self.inner_status.hash(state); + self.inner_stat.hash(state); + } +} + /// Parsing `/proc/self/stat` file. /// /// In some case, the first pair (and the only one pair) will contains whitespace, @@ -378,45 +343,40 @@ pub fn walk_process() -> impl Iterator { } #[cfg(test)] -#[cfg(target_os = "linux")] mod tests { + use crate::features::tty::Teletype; + use super::*; use std::str::FromStr; #[test] - fn test_tty_convention() { + fn test_tty_from() { + assert_eq!(Teletype::try_from("?").unwrap(), Teletype::Unknown); + assert_eq!(Teletype::try_from("/dev/tty1").unwrap(), Teletype::Tty(1)); + assert_eq!(Teletype::try_from("/dev/tty10").unwrap(), Teletype::Tty(10)); + assert_eq!(Teletype::try_from("/dev/pts/1").unwrap(), Teletype::Pts(1)); assert_eq!( - TerminalType::try_from("/dev/tty1").unwrap(), - TerminalType::Tty(1) + Teletype::try_from("/dev/pts/10").unwrap(), + Teletype::Pts(10) ); + assert_eq!(Teletype::try_from("/dev/ttyS1").unwrap(), Teletype::TtyS(1)); assert_eq!( - TerminalType::try_from("/dev/tty10").unwrap(), - TerminalType::Tty(10) - ); - assert_eq!( - TerminalType::try_from("/dev/pts/1").unwrap(), - TerminalType::Pts(1) - ); - assert_eq!( - TerminalType::try_from("/dev/pts/10").unwrap(), - TerminalType::Pts(10) - ); - assert_eq!( - TerminalType::try_from("/dev/ttyS1").unwrap(), - TerminalType::TtyS(1) - ); - assert_eq!( - TerminalType::try_from("/dev/ttyS10").unwrap(), - TerminalType::TtyS(10) - ); - assert_eq!( - TerminalType::try_from("ttyS10").unwrap(), - TerminalType::TtyS(10) + Teletype::try_from("/dev/ttyS10").unwrap(), + Teletype::TtyS(10) ); + assert_eq!(Teletype::try_from("ttyS10").unwrap(), Teletype::TtyS(10)); - assert!(TerminalType::try_from("value").is_err()); - assert!(TerminalType::try_from("TtyS10").is_err()); + assert!(Teletype::try_from("value").is_err()); + assert!(Teletype::try_from("TtyS10").is_err()); + } + + #[test] + fn test_terminal_type_display() { + assert_eq!(Teletype::Pts(10).to_string(), "/dev/pts/10"); + assert_eq!(Teletype::Tty(10).to_string(), "/dev/tty10"); + assert_eq!(Teletype::TtyS(10).to_string(), "/dev/ttyS10"); + assert_eq!(Teletype::Unknown.to_string(), "?"); } #[test] @@ -432,8 +392,6 @@ mod tests { assert!(RunState::try_from("G").is_err()); assert!(RunState::try_from("Rg").is_err()); - - assert!(RunState::try_from(String::from("Rg")).is_err()); } fn current_pid() -> usize { @@ -457,7 +415,7 @@ mod tests { } #[test] - fn test_process_information() { + fn test_pid_entry() { let current_pid = current_pid(); let mut pid_entry = ProcessInformation::try_new( @@ -470,16 +428,10 @@ mod tests { .flatten() .map(DirEntry::into_path) .flat_map(|it| it.read_link()) - .flat_map(TerminalType::try_from) + .flat_map(Teletype::try_from) .collect::>(); - assert_eq!(pid_entry.ttys().unwrap(), result.into()); - } - - #[test] - fn test_process_information_new() { - let result = ProcessInformation::try_new(PathBuf::from_iter(["/", "proc", "1"])); - assert!(result.is_ok()); + assert_eq!(pid_entry.ttys().unwrap(), result.into()) } #[test] diff --git a/src/uucore/src/lib/features/tty.rs b/src/uucore/src/lib/features/tty.rs new file mode 100644 index 000000000..49ec700da --- /dev/null +++ b/src/uucore/src/lib/features/tty.rs @@ -0,0 +1,93 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Set of functions to parsing TTY +use std::{ + fmt::{self, Display, Formatter}, + path::PathBuf, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Teletype { + Tty(u64), + TtyS(u64), + Pts(u64), + Unknown, +} + +impl Display for Teletype { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Tty(id) => write!(f, "/dev/pts/{}", id), + Self::TtyS(id) => write!(f, "/dev/tty{}", id), + Self::Pts(id) => write!(f, "/dev/ttyS{}", id), + Self::Unknown => write!(f, "?"), + } + } +} + +impl TryFrom for Teletype { + type Error = (); + + fn try_from(value: String) -> Result { + if value == "?" { + return Ok(Self::Unknown); + } + + Self::try_from(value.as_str()) + } +} + +impl TryFrom<&str> for Teletype { + type Error = (); + + fn try_from(value: &str) -> Result { + Self::try_from(PathBuf::from(value)) + } +} + +impl TryFrom for Teletype { + type Error = (); + + fn try_from(value: PathBuf) -> Result { + // Three case: /dev/pts/* , /dev/ttyS**, /dev/tty** + + let mut iter = value.iter(); + // Case 1 + + // Considering this format: **/**/pts/ + if let (Some(_), Some(num)) = (iter.find(|it| *it == "pts"), iter.next()) { + return num + .to_str() + .ok_or(())? + .parse::() + .map_err(|_| ()) + .map(Teletype::Pts); + }; + + // Considering this format: **/**/ttyS** then **/**/tty** + let path = value.to_str().ok_or(())?; + + let f = |prefix: &str| { + value + .iter() + .last()? + .to_str()? + .strip_prefix(prefix)? + .parse::() + .ok() + }; + + if path.contains("ttyS") { + // Case 2 + f("ttyS").ok_or(()).map(Teletype::TtyS) + } else if path.contains("tty") { + // Case 3 + f("tty").ok_or(()).map(Teletype::Tty) + } else { + Err(()) + } + } +}