Skip to content

Commit 316f8fc

Browse files
committed
process: Fix tty resolving
Current tty resolution can give wrong result e.g. if the process has closed all fds of its controlling terminal. The proper way to do it seems to be based on field in /proc/<pid>/stat, which gives a device number of the tty. Converting that to a device path requires parsing /proc/tty/drivers (according to strace of upstream ps). After this `ps`, `ps -x` and `ps -a` give same processes as upstream ps.
1 parent 833ea21 commit 316f8fc

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)