Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 83 additions & 9 deletions src/uu/pgrep/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ pub enum Teletype {
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::Tty(id) => write!(f, "/dev/tty{id}"),
Self::TtyS(id) => write!(f, "/dev/ttyS{id}"),
Self::Pts(id) => write!(f, "/dev/pts/{id}"),
Self::Unknown => write!(f, "?"),
}
}
Expand Down Expand Up @@ -99,6 +99,38 @@ impl TryFrom<PathBuf> for Teletype {
}
}

impl TryFrom<u64> for Teletype {
type Error = ();

fn try_from(tty_nr: u64) -> Result<Self, Self::Error> {
// tty_nr is 0 for processes without a controlling terminal
if tty_nr == 0 {
return Ok(Self::Unknown);
}

// Extract major and minor device numbers
// In Linux, tty_nr is encoded as: (major << 8) | minor
// However, for pts devices, the encoding is different: major is 136-143
let major = (tty_nr >> 8) & 0xFFF;
let minor = (tty_nr & 0xFF) | ((tty_nr >> 12) & 0xFFF00);

match major {
// Virtual console terminals (/dev/tty1, /dev/tty2, etc.)
4 => Ok(Self::Tty(minor)),
// Serial terminals (/dev/ttyS0, /dev/ttyS1, etc.)
5 => Ok(Self::TtyS(minor)),
// Pseudo-terminals (/dev/pts/0, /dev/pts/1, etc.)
// pts major numbers are 136-143
136..=143 => {
let pts_num = (major - 136) * 256 + minor;
Ok(Self::Pts(pts_num))
}
// Unknown terminal type
_ => Ok(Self::Unknown),
}
}
}

/// State of process
/// https://www.man7.org/linux/man-pages//man5/proc_pid_stat.5.html
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
Expand Down Expand Up @@ -482,6 +514,12 @@ impl ProcessInformation {
self.get_numeric_stat_field(5)
}

pub fn tty_nr(&mut self) -> Result<u64, io::Error> {
// the tty_nr is the seventh field in /proc/<PID>/stat
// (https://www.kernel.org/doc/html/latest/filesystems/proc.html#id10)
self.get_numeric_stat_field(6)
}

fn get_uid_or_gid_field(&mut self, field: &str, index: usize) -> Result<u32, io::Error> {
self.status()
.get(field)
Expand Down Expand Up @@ -591,13 +629,23 @@ impl ProcessInformation {
RunState::try_from(self.stat().get(2).unwrap().as_str())
}

/// This function will scan the `/proc/<pid>/fd` directory
/// Get the controlling terminal of the process.
///
/// If the process does not belong to any terminal and mismatched permission,
/// the result will contain [TerminalType::Unknown].
/// This function first tries to get the terminal from `/proc/<pid>/stat` (field 7, tty_nr)
/// which is world-readable and doesn't require special permissions.
/// Only if that fails, it falls back to scanning `/proc/<pid>/fd` directory.
///
/// Otherwise [TerminalType::Unknown] does not appear in the result.
pub fn tty(&self) -> Teletype {
/// Returns [Teletype::Unknown] if the process has no controlling terminal.
pub fn tty(&mut self) -> Teletype {
// First try to get tty_nr from stat file (always accessible)
if let Ok(tty_nr) = self.tty_nr() {
if let Ok(tty) = Teletype::try_from(tty_nr) {
return tty;
}
}

// Fall back to scanning /proc/<pid>/fd directory
// This requires permissions to read the process's fd directory
let path = PathBuf::from(format!("/proc/{}/fd", self.pid));

let Ok(result) = fs::read_dir(path) else {
Expand Down Expand Up @@ -730,6 +778,32 @@ mod tests {
#[cfg(target_os = "linux")]
use uucore::process::getpid;

#[test]
fn test_tty_nr_decoding() {
// Test no controlling terminal
assert_eq!(Teletype::try_from(0u64).unwrap(), Teletype::Unknown);

// Test virtual console terminals (/dev/tty1, /dev/tty2, etc.)
// tty1: major=4, minor=1 => (4 << 8) | 1 = 1025
assert_eq!(Teletype::try_from(1025u64).unwrap(), Teletype::Tty(1));
// tty12: major=4, minor=12 => (4 << 8) | 12 = 1036
assert_eq!(Teletype::try_from(1036u64).unwrap(), Teletype::Tty(12));

// Test serial terminals (/dev/ttyS0, /dev/ttyS1, etc.)
// ttyS0: major=5, minor=0 => (5 << 8) | 0 = 1280
assert_eq!(Teletype::try_from(1280u64).unwrap(), Teletype::TtyS(0));
// ttyS1: major=5, minor=1 => (5 << 8) | 1 = 1281
assert_eq!(Teletype::try_from(1281u64).unwrap(), Teletype::TtyS(1));

// Test pseudo-terminals (/dev/pts/0, /dev/pts/1, etc.)
// pts/0: major=136, minor=0 => (136 << 8) | 0 = 34816
assert_eq!(Teletype::try_from(34816u64).unwrap(), Teletype::Pts(0));
// pts/1: major=136, minor=1 => (136 << 8) | 1 = 34817
assert_eq!(Teletype::try_from(34817u64).unwrap(), Teletype::Pts(1));
// pts/256: major=137, minor=0 => (137 << 8) | 0 = 35072
assert_eq!(Teletype::try_from(35072u64).unwrap(), Teletype::Pts(256));
}

#[test]
fn test_run_state_conversion() {
assert_eq!(RunState::try_from("R").unwrap(), RunState::Running);
Expand Down Expand Up @@ -759,7 +833,7 @@ mod tests {
#[test]
#[cfg(target_os = "linux")]
fn test_pid_entry() {
let pid_entry = ProcessInformation::current_process_info().unwrap();
let mut pid_entry = ProcessInformation::current_process_info().unwrap();
let mut result = WalkDir::new(format!("/proc/{}/fd", getpid()))
.into_iter()
.flatten()
Expand Down
2 changes: 1 addition & 1 deletion src/uu/ps/src/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ fn sid(proc_info: RefCell<ProcessInformation>) -> String {
}

fn tty(proc_info: RefCell<ProcessInformation>) -> String {
match proc_info.borrow().tty() {
match proc_info.borrow_mut().tty() {
Teletype::Tty(tty) => format!("tty{tty}"),
Teletype::TtyS(ttys) => format!("ttyS{ttys}"),
Teletype::Pts(pts) => format!("pts/{pts}"),
Expand Down
5 changes: 4 additions & 1 deletion src/uu/snice/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ impl SelectedTarget {
let pid = pid.as_u32();
let path = PathBuf::from_str(&format!("/proc/{pid}/")).unwrap();

ProcessInformation::try_new(path).unwrap().tty() == *tty
ProcessInformation::try_new(path)
.map(|mut p| p.tty())
.unwrap_or(Teletype::Unknown)
== *tty
})
.map(|(pid, _)| pid.as_u32())
.collect()
Expand Down
7 changes: 5 additions & 2 deletions src/uu/snice/src/snice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ pub fn ask_user(pid: u32) -> bool {
let process = process_snapshot().process(Pid::from_u32(pid)).unwrap();

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

let user = process
Expand Down Expand Up @@ -214,7 +214,10 @@ pub fn construct_verbose_result(
row![pid]
}
Some((tty, user, cmd, action)) => {
row![tty.unwrap().tty(), user, pid, cmd, action]
let tty_str = tty
.map(|mut t| t.tty().to_string())
.unwrap_or_else(|_| "?".to_string());
row![tty_str, user, pid, cmd, action]
}
})
.collect::<Table>();
Expand Down
24 changes: 15 additions & 9 deletions tests/by-util/test_pgrep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,16 +260,22 @@ fn test_count_with_non_matching_pattern() {
#[test]
#[cfg(target_os = "linux")]
fn test_terminal() {
let re = &Regex::new(MULTIPLE_PIDS).unwrap();
// Test with unknown terminal (?) which should match processes without a terminal
// This is more reliable than testing tty1 which may not exist in CI
new_ucmd!()
.arg("-t")
.arg("?")
.arg("kthreadd") // kthreadd has no terminal
.succeeds()
.stdout_matches(&Regex::new(SINGLE_PID).unwrap());

for arg in ["-t", "--terminal"] {
new_ucmd!()
.arg(arg)
.arg("tty1")
.arg("--inverse") // XXX hack to make test pass in CI
.succeeds()
.stdout_matches(re);
}
// Test --inverse with unknown terminal to find processes WITH terminals
// In CI, there may be SSH or other processes with pts terminals
new_ucmd!()
.arg("--terminal")
.arg("?")
.arg("--inverse")
.succeeds(); // Just check it succeeds, don't verify specific output
}

#[test]
Expand Down
Loading