use std::collections::HashMap;
use std::fs::{self, read_dir, read_link};
use std::io::{Error, ErrorKind, Result};
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
use lazy_static::lazy_static;
use libc::{kill, sysconf};
use libc::{SIGKILL, _SC_CLK_TCK, _SC_PAGESIZE};
use crate::{GID, PID, UID};
lazy_static! {
static ref TICKS_PER_SECOND: f64 = { unsafe { sysconf(_SC_CLK_TCK) as f64 } };
static ref PAGE_SIZE: u64 = { unsafe { sysconf(_SC_PAGESIZE) as u64 } };
}
fn procfs_path(pid: PID, name: &str) -> PathBuf {
let mut path = PathBuf::new();
path.push("/proc");
path.push(&pid.to_string());
path.push(&name);
path
}
fn parse_error<P>(message: &str, path: P) -> Error
where
P: AsRef<Path>,
{
let path = path.as_ref().to_str().unwrap_or("unknown path");
Error::new(
ErrorKind::InvalidInput,
format!("{} (from {})", message, path),
)
}
#[derive(Clone, Copy, Debug)]
pub enum State {
Running,
Sleeping,
Waiting,
Stopped,
Traced,
Paging,
Dead,
Zombie,
Idle,
}
impl State {
pub fn from_char(state: char) -> Result<Self> {
match state {
'R' => Ok(State::Running),
'S' => Ok(State::Sleeping),
'D' => Ok(State::Waiting),
'T' => Ok(State::Stopped),
't' => Ok(State::Traced),
'W' => Ok(State::Paging),
'Z' => Ok(State::Zombie),
'X' => Ok(State::Dead),
'I' => Ok(State::Idle),
_ => Err(Error::new(
ErrorKind::InvalidInput,
format!("Invalid state character: {:?}", state),
)),
}
}
}
impl FromStr for State {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
if !s.len() == 1 {
Err(Error::new(
ErrorKind::InvalidInput,
format!("State is not a single character: {:?}", s),
))
} else {
State::from_char(s.chars().nth(0).unwrap())
}
}
}
impl ToString for State {
fn to_string(&self) -> String {
match *self {
State::Running => "R".to_string(),
State::Sleeping => "S".to_string(),
State::Waiting => "D".to_string(),
State::Stopped => "T".to_string(),
State::Traced => "t".to_string(),
State::Paging => "W".to_string(),
State::Zombie => "Z".to_string(),
State::Dead => "X".to_string(),
State::Idle => "I".to_string(),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Memory {
pub size: u64,
pub resident: u64,
pub share: u64,
pub text: u64,
pub data: u64,
}
impl Memory {
pub fn new(pid: PID) -> Result<Memory> {
let path = procfs_path(pid, "statm");
let statm = fs::read_to_string(&path)?;
let fields: Vec<&str> = statm.trim_end().split(' ').collect();
Ok(Memory {
size: Memory::parse_bytes(fields[0], &path)? * *PAGE_SIZE,
resident: Memory::parse_bytes(fields[1], &path)? * *PAGE_SIZE,
share: Memory::parse_bytes(fields[2], &path)? * *PAGE_SIZE,
text: Memory::parse_bytes(fields[3], &path)? * *PAGE_SIZE,
data: Memory::parse_bytes(fields[5], &path)? * *PAGE_SIZE,
})
}
fn parse_bytes(field: &str, path: &PathBuf) -> Result<u64> {
u64::from_str(field)
.map_err(|e| parse_error(&format!("Could not parse memory: {}", e), path))
}
}
pub struct Fd {
pub number: i32,
pub path: PathBuf,
}
#[derive(Clone, Debug)]
pub struct Process {
pub pid: PID,
pub uid: UID,
pub gid: GID,
pub comm: String,
pub state: State,
pub ppid: PID,
pub pgrp: i32,
pub session: i32,
pub tty_nr: i32,
pub tpgid: i32,
pub flags: u32,
pub minflt: u64,
pub cminflt: u64,
pub majflt: u64,
pub cmajflt: u64,
pub utime: f64,
pub utime_ticks: u64,
pub stime: f64,
pub stime_ticks: u64,
pub cutime: f64,
pub cutime_ticks: i64,
pub cstime: f64,
pub cstime_ticks: i64,
pub priority: i64,
pub nice: i64,
pub num_threads: i64,
pub starttime: f64,
pub starttime_ticks: u128,
pub vsize: u64,
pub rss: i64,
pub rsslim: u64,
startcode: u64,
endcode: u64,
startstack: u64,
kstkesp: u64,
kstkeip: u64,
signal: u64,
blocked: u64,
sigignore: u64,
sigcatch: u64,
pub wchan: u64,
pub exit_signal: i32,
pub processor: i32,
pub rt_priority: u32,
pub policy: u32,
pub delayacct_blkio: f64,
pub delayacct_blkio_ticks: u128,
pub guest_time: f64,
pub guest_time_ticks: u64,
pub cguest_time: f64,
pub cguest_time_ticks: i64,
start_data: u64,
end_data: u64,
start_brk: u64,
arg_start: u64,
arg_end: u64,
env_start: u64,
env_end: u64,
pub exit_code: i32,
}
impl Process {
pub fn new(pid: PID) -> Result<Process> {
let path = procfs_path(pid, "stat");
let stat = fs::read_to_string(&path)?;
let meta = fs::metadata(procfs_path(pid, ""))?;
Process::new_internal(&stat, meta.uid(), meta.gid(), &path)
}
fn new_internal<P>(stat: &str, file_uid: UID, file_gid: GID, path: P) -> Result<Process>
where
P: AsRef<Path>,
{
let (pid_, rest) = match stat.find('(') {
Some(i) => stat.split_at(i - 1),
None => return Err(parse_error("Could not parse comm", path)),
};
let (comm, rest) = match rest.rfind(')') {
Some(i) => rest.split_at(i + 2),
None => return Err(parse_error("Could not parse comm", path)),
};
let mut fields: Vec<&str> = Vec::new();
fields.push(pid_);
fields.push(&comm[2..comm.len() - 2]);
fields.extend(rest.trim_end().split(' '));
if fields.len() != 52 {
return Err(parse_error(
&format!("Expected 52 fields, got {}", fields.len()),
path,
));
}
Ok(Process {
pid: try_parse!(fields[00]),
uid: file_uid,
gid: file_gid,
comm: try_parse!(fields[1]),
state: try_parse!(fields[2]),
ppid: try_parse!(fields[3]),
pgrp: try_parse!(fields[4]),
session: try_parse!(fields[5]),
tty_nr: try_parse!(fields[6]),
tpgid: try_parse!(fields[7]),
flags: try_parse!(fields[8]),
minflt: try_parse!(fields[9]),
cminflt: try_parse!(fields[10]),
majflt: try_parse!(fields[11]),
cmajflt: try_parse!(fields[12]),
utime: try_parse!(fields[13], u64::from_str) as f64 / *TICKS_PER_SECOND,
utime_ticks: try_parse!(fields[13], u64::from_str),
stime: try_parse!(fields[14], u64::from_str) as f64 / *TICKS_PER_SECOND,
stime_ticks: try_parse!(fields[14], u64::from_str),
cutime: try_parse!(fields[15], i64::from_str) as f64 / *TICKS_PER_SECOND,
cutime_ticks: try_parse!(fields[15], i64::from_str),
cstime: try_parse!(fields[16], i64::from_str) as f64 / *TICKS_PER_SECOND,
cstime_ticks: try_parse!(fields[16], i64::from_str),
priority: try_parse!(fields[17]),
nice: try_parse!(fields[18]),
num_threads: try_parse!(fields[19]),
starttime: try_parse!(fields[21], u64::from_str) as f64 / *TICKS_PER_SECOND,
starttime_ticks: try_parse!(fields[21]),
vsize: try_parse!(fields[22]),
rss: try_parse!(fields[23], i64::from_str) * *PAGE_SIZE as i64,
rsslim: try_parse!(fields[24]),
startcode: try_parse!(fields[25]),
endcode: try_parse!(fields[26]),
startstack: try_parse!(fields[27]),
kstkesp: try_parse!(fields[28]),
kstkeip: try_parse!(fields[29]),
signal: try_parse!(fields[30]),
blocked: try_parse!(fields[31]),
sigignore: try_parse!(fields[32]),
sigcatch: try_parse!(fields[33]),
wchan: try_parse!(fields[34]),
exit_signal: try_parse!(fields[37]),
processor: try_parse!(fields[38]),
rt_priority: try_parse!(fields[39]),
policy: try_parse!(fields[40]),
delayacct_blkio: try_parse!(fields[41], u64::from_str) as f64 / *TICKS_PER_SECOND,
delayacct_blkio_ticks: try_parse!(fields[41]),
guest_time: try_parse!(fields[42], u64::from_str) as f64 / *TICKS_PER_SECOND,
guest_time_ticks: try_parse!(fields[42], u64::from_str),
cguest_time: try_parse!(fields[43], i64::from_str) as f64 / *TICKS_PER_SECOND,
cguest_time_ticks: try_parse!(fields[43], i64::from_str),
start_data: try_parse!(fields[44]),
end_data: try_parse!(fields[45]),
start_brk: try_parse!(fields[46]),
arg_start: try_parse!(fields[47]),
arg_end: try_parse!(fields[48]),
env_start: try_parse!(fields[49]),
env_end: try_parse!(fields[50]),
exit_code: try_parse!(fields[51]),
})
}
pub fn is_alive(&self) -> bool {
match self.state {
State::Zombie => false,
_ => true,
}
}
pub fn cmdline_vec(&self) -> Result<Option<Vec<String>>> {
let cmdline = fs::read_to_string(&procfs_path(self.pid, "cmdline"))?;
if cmdline.is_empty() {
return Ok(None);
}
let split = cmdline.split_terminator(|c: char| c == '\0' || c == ' ');
Ok(Some(split.map(|x| x.to_string()).collect()))
}
pub fn cmdline(&self) -> Result<Option<String>> {
Ok(self.cmdline_vec()?.and_then(|c| Some(c.join(" "))))
}
pub fn cwd(&self) -> Result<PathBuf> {
read_link(procfs_path(self.pid, "cwd"))
}
pub fn exe(&self) -> Result<PathBuf> {
read_link(procfs_path(self.pid, "exe"))
}
fn environ_internal(file_contents: &str) -> Result<HashMap<String, String>> {
let mut ret = HashMap::new();
for s in file_contents.split_terminator('\0') {
let vv: Vec<&str> = s.splitn(2, '=').collect();
if vv.len() != 2 {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("Item {} has too few fields", s),
));
}
ret.insert(vv[0].to_owned(), vv[1].to_owned());
}
Ok(ret)
}
pub fn environ(&self) -> Result<HashMap<String, String>> {
let path = procfs_path(self.pid, "environ");
let env = fs::read_to_string(&path)?;
Process::environ_internal(&env)
}
pub fn memory(&self) -> Result<Memory> {
Memory::new(self.pid)
}
pub fn open_fds(&self) -> Result<Vec<Fd>> {
let mut fds = Vec::new();
let entry_set = read_dir(procfs_path(self.pid, "fd"))?;
for entry in entry_set {
if let Ok(entry) = entry {
let path = entry.path();
let fd_number = path
.file_name()
.ok_or_else(|| parse_error("Could not read /proc entry", &path))?;
if let Ok(fd_path) = read_link(&path) {
fds.push(Fd {
number: fd_number.to_string_lossy().parse::<i32>().unwrap(),
path: fd_path,
})
}
}
}
Ok(fds)
}
pub fn kill(&self) -> Result<()> {
match unsafe { kill(self.pid, SIGKILL) } {
0 => Ok(()),
-1 => Err(Error::last_os_error()),
_ => unreachable!(),
}
}
}
impl PartialEq for Process {
fn eq(&self, other: &Process) -> bool {
(self.pid == other.pid) && (self.starttime == other.starttime)
}
}
pub fn all() -> Result<Vec<Process>> {
let mut processes = Vec::new();
for entry in read_dir("/proc")? {
let path = entry?.path();
let name = path
.file_name()
.ok_or_else(|| parse_error("Could not read /proc entry", &path))?;
if let Ok(pid) = PID::from_str(&name.to_string_lossy()) {
processes.push(Process::new(pid)?)
}
}
Ok(processes)
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn stat_52_fields() {
let file_contents = "1 (init) S 0 1 1 0 -1 4219136 48162 38210015093 1033 16767427 1781 2205 119189638 18012864 20 0 1 0 9 34451456 504 18446744073709551615 1 1 0 0 0 0 0 4096 536962595 0 0 0 17 0 0 0 189 0 0 0 0 0 0 0 0 0 0\n";
let p =
Process::new_internal(&file_contents, 0, 0, &PathBuf::from("/proc/1/stat")).unwrap();
assert_eq!(p.pid, 1);
assert_eq!(p.comm, "init");
assert_eq!(p.utime, 17.81);
}
#[test]
fn starttime_in_seconds_and_ticks() {
let file_contents = "1 (init) S 0 1 1 0 -1 4219136 48162 38210015093 1033 16767427 1781 2205 119189638 18012864 20 0 1 0 9 34451456 504 18446744073709551615 1 1 0 0 0 0 0 4096 536962595 0 0 0 17 0 0 0 189 0 0 0 0 0 0 0 0 0 0\n";
let p =
Process::new_internal(&file_contents, 0, 0, &PathBuf::from("/proc/1/stat")).unwrap();
if *TICKS_PER_SECOND == 100.0 {
assert_eq!(p.starttime, 0.09);
} else if *TICKS_PER_SECOND == 1000.0 {
assert_eq!(p.starttime, 0.009);
}
assert_eq!((p.starttime * *TICKS_PER_SECOND) as u64, 9);
assert_eq!(p.starttime_ticks, 9);
}
#[test]
fn environ() {
let fc = "HOME=/\0init=/sbin/init\0recovery=\0TERM=linux\0BOOT_IMAGE=/boot/vmlinuz-3.13.0-128-generic\0PATH=/sbin:/usr/sbin:/bin:/usr/bin\0PWD=/\0rootmnt=/root\0";
let e = Process::environ_internal(fc).unwrap();
assert_eq!(e["HOME"], "/");
assert_eq!(e["rootmnt"], "/root");
assert_eq!(e["recovery"], "");
}
fn get_process() -> Process {
Process::new(std::process::id() as i32).unwrap()
}
#[test]
fn process_alive() {
assert!(get_process().is_alive());
}
#[test]
fn process_cpu() {
let process = get_process();
assert!(process.utime >= 0.0);
assert!(process.stime >= 0.0);
assert!(process.cutime >= 0.0);
assert!(process.cstime >= 0.0);
}
#[test]
fn process_cmdline() {
assert!(get_process().cmdline().is_ok());
}
#[test]
fn process_cwd() {
assert!(get_process().cwd().is_ok());
}
#[test]
fn process_exe() {
assert!(get_process().exe().is_ok());
}
#[test]
fn process_memory() {
get_process().memory().unwrap();
}
#[test]
fn process_equality() {
assert_eq!(get_process(), get_process());
}
#[test]
fn process_inequality() {
assert!(get_process() != Process::new(1).unwrap());
}
#[test]
fn all() {
super::all().unwrap();
}
}