use crate::uid;
use procfs::{prelude::*, process};
use std::process::ExitCode;
pub struct ProcessInfo {
pub pid: i32,
pub comm: String,
pub cmdline: String,
pub stat: procfs::process::Stat,
pub euid: u32,
pub ruid: u32,
pub rgid: u32,
}
impl ProcessInfo {
pub fn match_text(&self, full: bool) -> &str {
if full { &self.cmdline } else { &self.comm }
}
}
pub struct MatchOptions {
pub pattern: String,
pub full: bool,
pub ignore_case: bool,
pub exact: bool,
pub inverse: bool,
pub newest: bool,
pub oldest: bool,
pub older: Option<f64>,
pub pid: Option<Vec<i32>>,
pub parent: Option<Vec<i32>>,
pub pgroup: Option<Vec<i32>>,
pub group: Option<Vec<u32>>,
pub session: Option<Vec<i32>>,
pub terminal: Option<Vec<String>>,
pub euid: Option<Vec<String>>,
pub uid: Option<Vec<String>>,
pub runstates: Option<Vec<char>>,
pub env: Option<String>,
}
enum EnvSpec {
NameOnly(String),
NameValue(String, String),
}
fn parse_env_spec(s: &str) -> EnvSpec {
match s.split_once('=') {
Some((k, v)) => EnvSpec::NameValue(k.to_string(), v.to_string()),
None => EnvSpec::NameOnly(s.to_string()),
}
}
fn process_matches_env(proc: &process::Process, spec: &EnvSpec) -> bool {
let environ = match proc.environ() {
Ok(e) => e,
Err(_) => return false,
};
use std::ffi::OsStr;
let want_key: &OsStr = match spec {
EnvSpec::NameOnly(k) | EnvSpec::NameValue(k, _) => OsStr::new(k),
};
let value = match environ.get(want_key) {
Some(v) => v,
None => return false,
};
match spec {
EnvSpec::NameOnly(_) => true,
EnvSpec::NameValue(_, v) => value == OsStr::new(v),
}
}
impl MatchOptions {
pub fn has_filter(&self) -> bool {
self.pid.is_some()
|| self.uid.is_some()
|| self.euid.is_some()
|| self.parent.is_some()
|| self.pgroup.is_some()
|| self.group.is_some()
|| self.session.is_some()
|| self.terminal.is_some()
|| self.runstates.is_some()
|| self.env.is_some()
}
}
pub fn resolve_uid(name: &str) -> Option<u32> {
uid::resolve_uid(name)
}
pub fn read_pidfile(path: &std::path::Path) -> Result<Vec<i32>, String> {
let text = std::fs::read_to_string(path)
.map_err(|e| format!("cannot read {}: {e}", path.display()))?;
let mut pids = Vec::new();
for (i, line) in text.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let pid: i32 = trimmed
.parse()
.map_err(|_| format!("{}:{}: not a PID: {trimmed}", path.display(), i + 1))?;
pids.push(pid);
}
Ok(pids)
}
fn tty_nr_to_name(tty_nr: i32) -> Option<String> {
if tty_nr == 0 {
return None;
}
let major = ((tty_nr >> 8) & 0xff) as u32;
let minor = ((tty_nr & 0xff) | ((tty_nr >> 12) & 0xfff00)) as u32;
match major {
4 if minor < 64 => Some(format!("tty{minor}")),
4 => Some(format!("ttyS{}", minor - 64)),
136..=143 => Some(format!("pts/{}", (major - 136) * 256 + minor)),
_ => Some(format!("{major}/{minor}")),
}
}
fn system_uptime_ticks() -> Option<u64> {
let uptime = procfs::Uptime::current().ok()?;
let ticks_per_sec = procfs::ticks_per_second();
Some((uptime.uptime * ticks_per_sec as f64) as u64)
}
pub fn find_matching_processes(
opts: &MatchOptions,
tool_name: &str,
) -> Result<Vec<ProcessInfo>, ExitCode> {
let pattern = &opts.pattern;
let regex_pattern = if opts.exact {
format!("^{pattern}$")
} else {
pattern.to_string()
};
let re = match regex::RegexBuilder::new(®ex_pattern)
.case_insensitive(opts.ignore_case)
.build()
{
Ok(re) => re,
Err(e) => {
eprintln!("{tool_name}: invalid pattern: {e}");
return Err(ExitCode::from(2));
}
};
let uid_filter: Option<Vec<u32>> = opts
.uid
.as_ref()
.map(|uids| uids.iter().filter_map(|u| resolve_uid(u)).collect());
let euid_filter: Option<Vec<u32>> = opts
.euid
.as_ref()
.map(|uids| uids.iter().filter_map(|u| resolve_uid(u)).collect());
let terminal_filter: Option<Vec<String>> =
opts.terminal.as_ref().map(|terms| {
terms
.iter()
.map(|t| t.strip_prefix("/dev/").unwrap_or(t).to_string())
.collect()
});
let env_spec: Option<EnvSpec> = opts.env.as_deref().map(parse_env_spec);
let my_pid = std::process::id() as i32;
let all_procs = match process::all_processes() {
Ok(iter) => iter,
Err(e) => {
eprintln!("{tool_name}: {e}");
return Err(ExitCode::from(3));
}
};
let uptime_ticks = system_uptime_ticks();
let mut matches: Vec<ProcessInfo> = Vec::new();
for proc_result in all_procs {
let proc = match proc_result {
Ok(p) => p,
Err(_) => continue,
};
if proc.pid() == my_pid {
continue;
}
let stat = match proc.stat() {
Ok(s) => s,
Err(_) => continue,
};
let cmdline_vec = proc.cmdline().unwrap_or_default();
let cmdline = cmdline_vec.join(" ");
let status = match proc.status() {
Ok(s) => s,
Err(_) => continue,
};
let info = ProcessInfo {
pid: stat.pid,
comm: stat.comm.clone(),
cmdline: if cmdline.is_empty() {
stat.comm.clone()
} else {
cmdline
},
euid: status.euid,
ruid: status.ruid,
rgid: status.rgid,
stat,
};
if let Some(ref pids) = opts.pid
&& !pids.contains(&info.pid)
{
continue;
}
if let Some(ref parents) = opts.parent
&& !parents.contains(&info.stat.ppid)
{
continue;
}
if let Some(ref pgroups) = opts.pgroup {
let pgrp = info.stat.pgrp;
if !pgroups.iter().any(|&pg| {
if pg == 0 {
pgrp == rustix::process::getpgrp().as_raw_nonzero().get()
} else {
pgrp == pg
}
}) {
continue;
}
}
if let Some(ref groups) = opts.group
&& !groups.contains(&info.rgid)
{
continue;
}
if let Some(ref sessions) = opts.session {
let sess = info.stat.session;
if !sessions.iter().any(|&s| {
if s == 0 {
sess == rustix::process::getsid(None)
.map(|s| s.as_raw_nonzero().get())
.unwrap_or(0)
} else {
sess == s
}
}) {
continue;
}
}
if let Some(ref terms) = terminal_filter {
let proc_tty = tty_nr_to_name(info.stat.tty_nr);
match proc_tty {
None => continue,
Some(ref tty_name) => {
if !terms.iter().any(|t| tty_name == t) {
continue;
}
}
}
}
if let Some(ref uids) = uid_filter
&& !uids.contains(&info.ruid)
{
continue;
}
if let Some(ref euids) = euid_filter
&& !euids.contains(&info.euid)
{
continue;
}
if let Some(ref states) = opts.runstates
&& !states.contains(&info.stat.state)
{
continue;
}
if let Some(ref spec) = env_spec
&& !process_matches_env(&proc, spec)
{
continue;
}
if let Some(older_secs) = opts.older
&& let Some(up_ticks) = uptime_ticks
{
let tps = procfs::ticks_per_second();
let age_secs = (up_ticks - info.stat.starttime) / tps;
if (age_secs as f64) < older_secs {
continue;
}
}
let text = info.match_text(opts.full);
let matched = if pattern.is_empty() {
true
} else {
re.is_match(text)
};
let matched = if opts.inverse { !matched } else { matched };
if matched {
matches.push(info);
}
}
matches.sort_by_key(|p| p.pid);
if opts.newest {
if let Some(newest) = matches.iter().max_by_key(|p| p.stat.starttime) {
let pid = newest.pid;
matches.retain(|p| p.pid == pid);
}
} else if opts.oldest
&& let Some(oldest) = matches.iter().min_by_key(|p| p.stat.starttime)
{
let pid = oldest.pid;
matches.retain(|p| p.pid == pid);
}
Ok(matches)
}