| // Copyright 2023 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use std::fmt::Display; |
| use std::fs::File; |
| use std::io; |
| use std::io::BufRead; |
| use std::io::BufReader; |
| use std::io::Read; |
| use std::path::Path; |
| |
| use crate::ProcessId; |
| use crate::ThreadId; |
| |
| pub type Result<T> = std::result::Result<T, Error>; |
| |
| #[derive(Debug)] |
| pub enum Error { |
| Io(io::Error), |
| NotFound, |
| FormatCorrupt, |
| } |
| |
| impl std::error::Error for Error { |
| fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { |
| match self { |
| Self::Io(e) => Some(e), |
| _ => None, |
| } |
| } |
| } |
| |
| impl Display for Error { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| match self { |
| Self::Io(e) => e.fmt(f), |
| Self::NotFound => f.write_str("process/thread not found"), |
| Self::FormatCorrupt => f.write_str("stat file is corrupt"), |
| } |
| } |
| } |
| |
| impl From<io::Error> for Error { |
| fn from(e: io::Error) -> Self { |
| if e.kind() == io::ErrorKind::NotFound || e.raw_os_error() == Some(libc::ESRCH) { |
| Self::NotFound |
| } else { |
| Self::Io(e) |
| } |
| } |
| } |
| |
| pub fn load_process_timestamp(process_id: ProcessId) -> Result<u64> { |
| load_starttime(Path::new(&format!("/proc/{}/stat", process_id.0))) |
| } |
| |
| pub fn load_thread_timestamp(process_id: ProcessId, thread_id: ThreadId) -> Result<u64> { |
| load_starttime(Path::new(&format!( |
| "/proc/{}/task/{}/stat", |
| process_id.0, thread_id.0 |
| ))) |
| } |
| |
| fn load_starttime(path: &Path) -> Result<u64> { |
| let mut stat_file = File::open(path)?; |
| // starttime is the 22th column in /proc/pid/stat. Each numeric column in /proc/pid/stat has at |
| // most 21 bytes. (1 byte for sign + 19 bytes for u64 + 1 byte space). The 2nd column (comm) is |
| // at most 67 bytes including the wrapping parenthesis (proc_task_name() of kernel uses 64 bytes |
| // buffer `tcomm`). 512 bytes is enough to hold the 22 columns (i.e. 512 >= 21 * 21 + 67 = 508). |
| let mut buf = [0; 512]; |
| let n = stat_file.read(&mut buf)?; |
| |
| // Since threads can set comm by writing to /proc/self/task/tid/comm, it can contain any byte |
| // sequence. This means we need to exclusively look at kernel controlled bytes to determine |
| // where comm ends. To do this, we can scan backwards to look for the closing parentheses |
| // emitted by the kernel. |
| // The longest possible tail offset of comm is 88 bytes (= 21 bytes for pid column + 1 byte for |
| // space + 66 bytes for comm column). This works even if the stat file size is less than 88 |
| // bytes because the space after the stat file content are zeroed. |
| let mut i_comm_tail = None; |
| for (i, c) in buf[..88].iter().enumerate().rev() { |
| if *c == b')' { |
| i_comm_tail = Some(i); |
| break; |
| } |
| } |
| let Some(i_comm_tail) = i_comm_tail else { |
| return Err(Error::FormatCorrupt); |
| }; |
| |
| let mut prev_space = i_comm_tail + 1; |
| let mut starttime = None; |
| // `pid` and `comm` columns are consumed. |
| let mut column_idx = 2; |
| for (i, c) in buf[..n].iter().enumerate().skip(prev_space + 1) { |
| if *c == b' ' { |
| if column_idx == 21 { |
| // starttime is at 22th column. |
| starttime = Some(prev_space + 1..i); |
| break; |
| } |
| prev_space = i; |
| column_idx += 1; |
| } |
| } |
| let Some(starttime) = starttime else { |
| return Err(Error::FormatCorrupt); |
| }; |
| let starttime = std::str::from_utf8(&buf[starttime]).map_err(|_| Error::FormatCorrupt)?; |
| let starttime = starttime.parse().map_err(|_| Error::FormatCorrupt)?; |
| |
| Ok(starttime) |
| } |
| |
| pub fn load_tgid(thread_id: ThreadId) -> Result<ProcessId> { |
| let file = File::open(format!("/proc/{}/status", thread_id.0))?; |
| let r = BufReader::with_capacity(1024, file); |
| for line in r.lines() { |
| let line = line.map_err(Error::Io)?; |
| const TGID_TAG: &str = "Tgid:"; |
| if let Some(tgid) = line.strip_prefix(TGID_TAG) { |
| let tgid = tgid.trim().parse().map_err(|_| Error::FormatCorrupt)?; |
| return Ok(ProcessId(tgid)); |
| } |
| } |
| Err(Error::FormatCorrupt) |
| } |
| |
| pub struct ThreadChecker { |
| prefix: String, |
| } |
| |
| impl ThreadChecker { |
| pub fn new(process_id: ProcessId) -> Self { |
| // "/proc/" (6 bytes) + pid (at most 10 bytes) "/task/" (6 bytes) + tid (at most 10 bytes). |
| let mut prefix = String::with_capacity(32); |
| prefix.push_str("/proc/"); |
| prefix.push_str(&process_id.0.to_string()); |
| prefix.push_str("/task/"); |
| Self { prefix } |
| } |
| |
| pub fn thread_exists(&mut self, thread_id: ThreadId) -> bool { |
| let original_len = self.prefix.len(); |
| self.prefix.push_str(&thread_id.0.to_string()); |
| let result = Path::new(&self.prefix).exists(); |
| self.prefix.truncate(original_len); |
| result |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use std::io::Write; |
| |
| use super::*; |
| use crate::test_utils::*; |
| |
| #[test] |
| fn test_load_process_timestamp() { |
| let process_id = ProcessId(std::process::id()); |
| assert!(load_process_timestamp(process_id).unwrap() > 0); |
| |
| let (process_id, _, _process) = fork_process_for_test(); |
| assert!(load_process_timestamp(process_id).unwrap() > 0); |
| } |
| |
| #[test] |
| fn test_load_process_timestamp_not_found() { |
| let (process_id, _, process) = fork_process_for_test(); |
| drop(process); |
| assert!(matches!( |
| load_process_timestamp(process_id).err().unwrap(), |
| Error::NotFound |
| )); |
| } |
| |
| #[test] |
| fn test_load_thread_timestamp() { |
| let process_id = ProcessId(std::process::id()); |
| let thread_id = get_current_thread_id(); |
| assert!(load_thread_timestamp(process_id, thread_id).unwrap() > 0); |
| |
| let (thread_id, _thread) = spawn_thread_for_test(); |
| assert!(load_thread_timestamp(process_id, thread_id).unwrap() > 0); |
| |
| let (process_id, thread_id, _process) = fork_process_for_test(); |
| assert!(load_thread_timestamp(process_id, thread_id).unwrap() > 0); |
| } |
| |
| #[test] |
| fn test_load_thread_timestamp_not_found() { |
| let process_id = ProcessId(std::process::id()); |
| let thread_id = get_current_thread_id(); |
| let (child_process_id, child_thread_id, _process) = fork_process_for_test(); |
| |
| assert!(matches!( |
| load_thread_timestamp(process_id, child_thread_id) |
| .err() |
| .unwrap(), |
| Error::NotFound |
| )); |
| assert!(matches!( |
| load_thread_timestamp(child_process_id, thread_id) |
| .err() |
| .unwrap(), |
| Error::NotFound |
| )); |
| } |
| |
| #[test] |
| fn test_load_starttime() { |
| for (stat_file_content, starttime) in [ |
| ( |
| "9345 (resourced) S 1 9344 9344 0 -1 1077936384 599 0 0 0 2851 2468 0 0 20 0 1 0 \ |
| 41329081 19865600 2719 18446744073709551615 101386084483072 101386086716560 \ |
| 140736188509360 0 0 0 0 4096 1088 1 0 0 17 0 0 0 0 0 0 101386086981672 101386086982232 \ |
| 101386110365696 140736188513963 140736188513982 140736188513982 140736188514277 0", |
| 41329081, |
| ), |
| // Shortest stat file. |
| ("1 (a) 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 ", 2), |
| // Longest pid + comm stat file. |
| ( |
| "1234567890123456789 \ |
| (1234567890123456789012345678901234567890123456789012345678901234) 3 4 5 6 7 8 9 0 1 2 \ |
| 3 4 5 6 7 8 9 0 1 123 ", |
| 123, |
| ), |
| // comm contains spaces and parenthesis. |
| ( |
| "1 ( a ( b ) c ) ) 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 456 ", |
| 456, |
| ), |
| ] { |
| let mut file = tempfile::NamedTempFile::new().unwrap(); |
| file.write_all(stat_file_content.as_bytes()).unwrap(); |
| assert_eq!( |
| load_starttime(file.path()).unwrap(), |
| starttime, |
| "{}", |
| stat_file_content |
| ); |
| } |
| } |
| |
| #[test] |
| fn test_load_starttime_corrupt_format() { |
| for stat_file_content in [ |
| // no comm parenthesis. |
| "1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2", |
| // starttime is not a number. |
| "1 (a) 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 a ", |
| // no 22th space |
| "1 (a) 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2", |
| ] { |
| let mut file = tempfile::NamedTempFile::new().unwrap(); |
| file.write_all(stat_file_content.as_bytes()).unwrap(); |
| assert!( |
| matches!( |
| load_starttime(file.path()).err().unwrap(), |
| Error::FormatCorrupt |
| ), |
| "{}", |
| stat_file_content |
| ); |
| } |
| |
| let mut file = tempfile::NamedTempFile::new().unwrap(); |
| file.write_all(b"1 (a) 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2") |
| .unwrap(); |
| // non utf-8 |
| file.write_all(&[255, b' ']).unwrap(); |
| assert!(matches!( |
| load_starttime(file.path()).err().unwrap(), |
| Error::FormatCorrupt |
| )); |
| } |
| |
| #[test] |
| fn test_load_tgid() { |
| let process_id = ProcessId(std::process::id()); |
| assert_eq!(load_tgid(ThreadId(process_id.0)).unwrap(), process_id); |
| |
| let (thread_id, thread) = spawn_thread_for_test(); |
| assert_eq!(load_tgid(thread_id).unwrap(), process_id); |
| drop(thread); |
| wait_for_thread_removed(process_id, thread_id); |
| assert!(matches!(load_tgid(thread_id), Err(Error::NotFound))); |
| |
| let (process_id, thread_id, _process) = fork_process_for_test(); |
| assert_eq!(load_tgid(thread_id).unwrap(), process_id); |
| } |
| |
| #[test] |
| fn test_thread_exists() { |
| let process_id = ProcessId(std::process::id()); |
| let mut checker = ThreadChecker::new(process_id); |
| let (thread_id, thread) = spawn_thread_for_test(); |
| let (another_process_id, another_thread_id, process) = fork_process_for_test(); |
| let mut another_checker = ThreadChecker::new(another_process_id); |
| |
| assert!(checker.thread_exists(thread_id)); |
| assert!(!checker.thread_exists(another_thread_id)); |
| assert!(another_checker.thread_exists(another_thread_id)); |
| assert!(!another_checker.thread_exists(thread_id)); |
| drop(thread); |
| wait_for_thread_removed(process_id, thread_id); |
| assert!(!checker.thread_exists(thread_id)); |
| drop(process); |
| assert!(!another_checker.thread_exists(another_thread_id)); |
| } |
| } |