Skip to content

Commit 883207c

Browse files
authored
Merge pull request #578 from dezgeg/tty_fix
process: Fix tty resolving
2 parents 833ea21 + 316f8fc commit 883207c

File tree

3 files changed

+192
-70
lines changed

3 files changed

+192
-70
lines changed

src/uu/pgrep/src/process.rs

Lines changed: 183 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use regex::Regex;
77
use std::fs::read_link;
88
use std::hash::Hash;
9+
#[cfg(target_os = "linux")]
10+
use std::ops::RangeInclusive;
911
use std::sync::{LazyLock, OnceLock};
1012
use std::{
1113
collections::HashMap,
@@ -15,20 +17,130 @@ use std::{
1517
};
1618
use walkdir::{DirEntry, WalkDir};
1719

20+
/// Represents a TTY driver entry from /proc/tty/drivers
21+
#[cfg(target_os = "linux")]
22+
#[derive(Debug, Clone, PartialEq, Eq)]
23+
struct TtyDriverEntry {
24+
device_prefix: String,
25+
major: u32,
26+
minor_range: RangeInclusive<u32>,
27+
}
28+
29+
#[cfg(target_os = "linux")]
30+
impl TtyDriverEntry {
31+
fn new(device_prefix: String, major: u32, minor_range: RangeInclusive<u32>) -> Self {
32+
Self {
33+
device_prefix,
34+
major,
35+
minor_range,
36+
}
37+
}
38+
39+
fn device_path_if_matches(&self, major: u32, minor: u32) -> Option<String> {
40+
if self.major != major || !self.minor_range.contains(&minor) {
41+
return None;
42+
}
43+
44+
// /dev/pts devices are in a subdirectory unlike others
45+
if self.device_prefix == "/dev/pts" {
46+
return Some(format!("/dev/pts/{}", minor));
47+
}
48+
49+
// If there is only one minor (e.g. /dev/console) it should not get a number
50+
if self.minor_range.start() == self.minor_range.end() {
51+
Some(self.device_prefix.clone())
52+
} else {
53+
let device_number = minor - self.minor_range.start();
54+
Some(format!("{}{}", self.device_prefix, device_number))
55+
}
56+
}
57+
}
58+
59+
#[cfg(target_os = "linux")]
60+
static TTY_DRIVERS_CACHE: LazyLock<Vec<TtyDriverEntry>> = LazyLock::new(|| {
61+
fs::read_to_string("/proc/tty/drivers")
62+
.map(|content| parse_proc_tty_drivers(&content))
63+
.unwrap_or_default()
64+
});
65+
66+
#[cfg(target_os = "linux")]
67+
fn parse_proc_tty_drivers(drivers_content: &str) -> Vec<TtyDriverEntry> {
68+
// Example lines:
69+
// /dev/tty /dev/tty 5 0 system:/dev/tty
70+
// /dev/vc/0 /dev/vc/0 4 0 system:vtmaster
71+
// hvc /dev/hvc 229 0-7 system
72+
// serial /dev/ttyS 4 64-95 serial
73+
// pty_slave /dev/pts 136 0-1048575 pty:slave
74+
let regex = Regex::new(r"^[^ ]+ +([^ ]+) +(\d+) +(\d+)(?:-(\d+))?").unwrap();
75+
76+
let mut entries = Vec::new();
77+
for line in drivers_content.lines() {
78+
let Some(captures) = regex.captures(line) else {
79+
continue;
80+
};
81+
82+
let device_prefix = captures[1].to_string();
83+
let Ok(major) = captures[2].parse::<u32>() else {
84+
continue;
85+
};
86+
let Ok(min_minor) = captures[3].parse::<u32>() else {
87+
continue;
88+
};
89+
let max_minor = captures
90+
.get(4)
91+
.and_then(|m| m.as_str().parse::<u32>().ok())
92+
.unwrap_or(min_minor);
93+
94+
entries.push(TtyDriverEntry::new(
95+
device_prefix,
96+
major,
97+
min_minor..=max_minor,
98+
));
99+
}
100+
101+
entries
102+
}
103+
18104
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19105
pub enum Teletype {
20-
Tty(u64),
21-
TtyS(u64),
22-
Pts(u64),
106+
Known(String),
23107
Unknown,
24108
}
25109

110+
impl Teletype {
111+
#[cfg(target_os = "linux")]
112+
pub fn from_tty_nr(tty_nr: u64) -> Self {
113+
Self::from_tty_nr_impl(tty_nr, &TTY_DRIVERS_CACHE)
114+
}
115+
116+
#[cfg(not(target_os = "linux"))]
117+
pub fn from_tty_nr(_tty_nr: u64) -> Self {
118+
Self::Unknown
119+
}
120+
121+
#[cfg(target_os = "linux")]
122+
fn from_tty_nr_impl(tty_nr: u64, drivers: &[TtyDriverEntry]) -> Self {
123+
use uucore::libc::{major, minor};
124+
125+
if tty_nr == 0 {
126+
return Self::Unknown;
127+
}
128+
129+
let (major_dev, minor_dev) = (major(tty_nr), minor(tty_nr));
130+
for entry in drivers.iter() {
131+
if let Some(device_path) = entry.device_path_if_matches(major_dev, minor_dev) {
132+
return Self::Known(device_path);
133+
}
134+
}
135+
136+
Self::Unknown
137+
}
138+
}
139+
26140
impl Display for Teletype {
27141
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
28142
match self {
29-
Self::Tty(id) => write!(f, "/dev/pts/{id}"),
30-
Self::TtyS(id) => write!(f, "/dev/tty{id}"),
31-
Self::Pts(id) => write!(f, "/dev/ttyS{id}"),
143+
Self::Known(device_path) => write!(f, "{}", device_path),
32144
Self::Unknown => write!(f, "?"),
33145
}
34146
}
@@ -58,43 +170,8 @@ impl TryFrom<PathBuf> for Teletype {
58170
type Error = ();
59171

60172
fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
61-
// Three case: /dev/pts/* , /dev/ttyS**, /dev/tty**
62-
63-
let mut iter = value.iter();
64-
// Case 1
65-
66-
// Considering this format: **/**/pts/<num>
67-
if let (Some(_), Some(num)) = (iter.find(|it| *it == "pts"), iter.next()) {
68-
return num
69-
.to_str()
70-
.ok_or(())?
71-
.parse::<u64>()
72-
.map_err(|_| ())
73-
.map(Teletype::Pts);
74-
};
75-
76-
// Considering this format: **/**/ttyS** then **/**/tty**
77-
let path = value.to_str().ok_or(())?;
78-
79-
let f = |prefix: &str| {
80-
value
81-
.iter()
82-
.next_back()?
83-
.to_str()?
84-
.strip_prefix(prefix)?
85-
.parse::<u64>()
86-
.ok()
87-
};
88-
89-
if path.contains("ttyS") {
90-
// Case 2
91-
f("ttyS").ok_or(()).map(Teletype::TtyS)
92-
} else if path.contains("tty") {
93-
// Case 3
94-
f("tty").ok_or(()).map(Teletype::Tty)
95-
} else {
96-
Err(())
97-
}
173+
let path_str = value.to_str().ok_or(())?;
174+
Ok(Self::Known(path_str.to_string()))
98175
}
99176
}
100177

@@ -602,28 +679,17 @@ impl ProcessInformation {
602679
RunState::try_from(self.stat().get(2).unwrap().as_str())
603680
}
604681

605-
/// This function will scan the `/proc/<pid>/fd` directory
606-
///
607-
/// If the process does not belong to any terminal and mismatched permission,
608-
/// the result will contain [TerminalType::Unknown].
682+
/// Get the controlling terminal from the tty_nr field in /proc/<pid>/stat
609683
///
610-
/// Otherwise [TerminalType::Unknown] does not appear in the result.
611-
pub fn tty(&self) -> Teletype {
612-
let path = PathBuf::from(format!("/proc/{}/fd", self.pid));
613-
614-
let Ok(result) = fs::read_dir(path) else {
615-
return Teletype::Unknown;
684+
/// Returns Teletype::Unknown if the process has no controlling terminal (tty_nr == 0)
685+
/// or if the tty_nr cannot be resolved to a device.
686+
pub fn tty(&mut self) -> Teletype {
687+
let tty_nr = match self.get_numeric_stat_field(6) {
688+
Ok(tty_nr) => tty_nr,
689+
Err(_) => return Teletype::Unknown,
616690
};
617691

618-
for dir in result.flatten().filter(|it| it.path().is_symlink()) {
619-
if let Ok(path) = fs::read_link(dir.path()) {
620-
if let Ok(tty) = Teletype::try_from(path) {
621-
return tty;
622-
}
623-
}
624-
}
625-
626-
Teletype::Unknown
692+
Teletype::from_tty_nr(tty_nr)
627693
}
628694

629695
pub fn thread_ids(&mut self) -> &[usize] {
@@ -734,6 +800,53 @@ mod tests {
734800
#[cfg(target_os = "linux")]
735801
use uucore::process::getpid;
736802

803+
#[test]
804+
#[cfg(target_os = "linux")]
805+
fn test_tty_resolution() {
806+
let test_content = r#"/dev/tty /dev/tty 5 0 system:/dev/tty
807+
/dev/console /dev/console 5 1 system:console
808+
/dev/ptmx /dev/ptmx 5 2 system
809+
/dev/vc/0 /dev/vc/0 4 0 system:vtmaster
810+
hvc /dev/hvc 229 0-7 system
811+
serial /dev/ttyS 4 64-95 serial
812+
pty_slave /dev/pts 136 0-1048575 pty:slave
813+
pty_master /dev/ptm 128 0-1048575 pty:master
814+
unknown /dev/tty 4 1-63 console"#;
815+
816+
let expected_entries = vec![
817+
TtyDriverEntry::new("/dev/tty".to_string(), 5, 0..=0),
818+
TtyDriverEntry::new("/dev/console".to_string(), 5, 1..=1),
819+
TtyDriverEntry::new("/dev/ptmx".to_string(), 5, 2..=2),
820+
TtyDriverEntry::new("/dev/vc/0".to_string(), 4, 0..=0),
821+
TtyDriverEntry::new("/dev/hvc".to_string(), 229, 0..=7),
822+
TtyDriverEntry::new("/dev/ttyS".to_string(), 4, 64..=95),
823+
TtyDriverEntry::new("/dev/pts".to_string(), 136, 0..=1048575),
824+
TtyDriverEntry::new("/dev/ptm".to_string(), 128, 0..=1048575),
825+
TtyDriverEntry::new("/dev/tty".to_string(), 4, 1..=63),
826+
];
827+
828+
let parsed_entries = parse_proc_tty_drivers(test_content);
829+
assert_eq!(parsed_entries, expected_entries);
830+
831+
let test_cases = vec![
832+
// (major, minor, expected_result)
833+
(0, 0, Teletype::Unknown),
834+
(5, 0, Teletype::Known("/dev/tty".to_string())),
835+
(5, 1, Teletype::Known("/dev/console".to_string())),
836+
(136, 123, Teletype::Known("/dev/pts/123".to_string())),
837+
(4, 64, Teletype::Known("/dev/ttyS0".to_string())),
838+
(4, 65, Teletype::Known("/dev/ttyS1".to_string())),
839+
(229, 3, Teletype::Known("/dev/hvc3".to_string())),
840+
(999, 999, Teletype::Unknown),
841+
];
842+
843+
for (major, minor, expected) in test_cases {
844+
let tty_nr = uucore::libc::makedev(major, minor);
845+
let result = Teletype::from_tty_nr_impl(tty_nr, &parsed_entries);
846+
assert_eq!(result, expected);
847+
}
848+
}
849+
737850
#[test]
738851
fn test_run_state_conversion() {
739852
assert_eq!(RunState::try_from("R").unwrap(), RunState::Running);
@@ -763,7 +876,14 @@ mod tests {
763876
#[test]
764877
#[cfg(target_os = "linux")]
765878
fn test_pid_entry() {
766-
let pid_entry = ProcessInformation::current_process_info().unwrap();
879+
use std::io::IsTerminal;
880+
881+
let mut pid_entry = ProcessInformation::current_process_info().unwrap();
882+
883+
if !std::io::stdout().is_terminal() && !std::io::stderr().is_terminal() {
884+
assert_eq!(pid_entry.tty(), Teletype::Unknown);
885+
return;
886+
}
767887
let mut result = WalkDir::new(format!("/proc/{}/fd", getpid()))
768888
.into_iter()
769889
.flatten()

src/uu/ps/src/picker.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,11 @@ fn sid(proc_info: RefCell<ProcessInformation>) -> String {
135135
}
136136

137137
fn tty(proc_info: RefCell<ProcessInformation>) -> String {
138-
match proc_info.borrow().tty() {
139-
Teletype::Tty(tty) => format!("tty{tty}"),
140-
Teletype::TtyS(ttys) => format!("ttyS{ttys}"),
141-
Teletype::Pts(pts) => format!("pts/{pts}"),
138+
match proc_info.borrow_mut().tty() {
139+
Teletype::Known(device_path) => device_path
140+
.strip_prefix("/dev/")
141+
.unwrap_or(&device_path)
142+
.to_owned(),
142143
Teletype::Unknown => "?".to_owned(),
143144
}
144145
}

src/uu/snice/src/snice.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ pub fn ask_user(pid: u32) -> bool {
129129
let process = process_snapshot().process(Pid::from_u32(pid)).unwrap();
130130

131131
let tty = ProcessInformation::try_new(PathBuf::from_str(&format!("/proc/{pid}")).unwrap())
132-
.map(|v| v.tty().to_string())
132+
.map(|mut v| v.tty().to_string())
133133
.unwrap_or(String::from("?"));
134134

135135
let user = process
@@ -188,7 +188,8 @@ pub fn construct_verbose_result(
188188
let process = process_snapshot().process(Pid::from_u32(pid)).unwrap();
189189

190190
let tty =
191-
ProcessInformation::try_new(PathBuf::from_str(&format!("/proc/{pid}")).unwrap());
191+
ProcessInformation::try_new(PathBuf::from_str(&format!("/proc/{pid}")).unwrap())
192+
.map(|mut v| v.tty().to_string());
192193

193194
let user = process
194195
.user_id()
@@ -214,7 +215,7 @@ pub fn construct_verbose_result(
214215
row![pid]
215216
}
216217
Some((tty, user, cmd, action)) => {
217-
row![tty.unwrap().tty(), user, pid, cmd, action]
218+
row![tty.unwrap(), user, pid, cmd, action]
218219
}
219220
})
220221
.collect::<Table>();

0 commit comments

Comments
 (0)