procutils_common/
procmatch.rs1use crate::uid;
19use procfs::{prelude::*, process};
20use std::process::ExitCode;
21
22pub struct ProcessInfo {
27 pub pid: i32,
28 pub comm: String,
29 pub cmdline: String,
30 pub stat: procfs::process::Stat,
31 pub euid: u32,
32 pub ruid: u32,
33 pub rgid: u32,
34}
35
36impl ProcessInfo {
37 pub fn match_text(&self, full: bool) -> &str {
41 if full { &self.cmdline } else { &self.comm }
42 }
43}
44
45pub struct MatchOptions {
51 pub pattern: String,
52 pub full: bool,
53 pub ignore_case: bool,
54 pub exact: bool,
55 pub inverse: bool,
56 pub newest: bool,
57 pub oldest: bool,
58 pub older: Option<f64>,
59 pub pid: Option<Vec<i32>>,
60 pub parent: Option<Vec<i32>>,
61 pub pgroup: Option<Vec<i32>>,
62 pub group: Option<Vec<u32>>,
63 pub session: Option<Vec<i32>>,
64 pub terminal: Option<Vec<String>>,
65 pub euid: Option<Vec<String>>,
66 pub uid: Option<Vec<String>>,
67 pub runstates: Option<Vec<char>>,
68 pub env: Option<String>,
71}
72
73enum EnvSpec {
74 NameOnly(String),
75 NameValue(String, String),
76}
77
78fn parse_env_spec(s: &str) -> EnvSpec {
79 match s.split_once('=') {
80 Some((k, v)) => EnvSpec::NameValue(k.to_string(), v.to_string()),
81 None => EnvSpec::NameOnly(s.to_string()),
82 }
83}
84
85fn process_matches_env(proc: &process::Process, spec: &EnvSpec) -> bool {
86 let environ = match proc.environ() {
87 Ok(e) => e,
88 Err(_) => return false,
89 };
90 use std::ffi::OsStr;
91 let want_key: &OsStr = match spec {
92 EnvSpec::NameOnly(k) | EnvSpec::NameValue(k, _) => OsStr::new(k),
93 };
94 let value = match environ.get(want_key) {
95 Some(v) => v,
96 None => return false,
97 };
98 match spec {
99 EnvSpec::NameOnly(_) => true,
100 EnvSpec::NameValue(_, v) => value == OsStr::new(v),
101 }
102}
103
104impl MatchOptions {
105 pub fn has_filter(&self) -> bool {
108 self.pid.is_some()
109 || self.uid.is_some()
110 || self.euid.is_some()
111 || self.parent.is_some()
112 || self.pgroup.is_some()
113 || self.group.is_some()
114 || self.session.is_some()
115 || self.terminal.is_some()
116 || self.runstates.is_some()
117 || self.env.is_some()
118 }
119}
120
121pub fn resolve_uid(name: &str) -> Option<u32> {
124 uid::resolve_uid(name)
125}
126
127pub fn read_pidfile(path: &std::path::Path) -> Result<Vec<i32>, String> {
131 let text = std::fs::read_to_string(path)
132 .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
133 let mut pids = Vec::new();
134 for (i, line) in text.lines().enumerate() {
135 let trimmed = line.trim();
136 if trimmed.is_empty() {
137 continue;
138 }
139 let pid: i32 = trimmed.parse().map_err(|_| {
140 format!("{}:{}: not a PID: {trimmed}", path.display(), i + 1)
141 })?;
142 pids.push(pid);
143 }
144 Ok(pids)
145}
146
147fn tty_nr_to_name(tty_nr: i32) -> Option<String> {
148 if tty_nr == 0 {
149 return None;
150 }
151 let major = ((tty_nr >> 8) & 0xff) as u32;
152 let minor = ((tty_nr & 0xff) | ((tty_nr >> 12) & 0xfff00)) as u32;
153 match major {
154 4 if minor < 64 => Some(format!("tty{minor}")),
155 4 => Some(format!("ttyS{}", minor - 64)),
156 136..=143 => Some(format!("pts/{}", (major - 136) * 256 + minor)),
157 _ => Some(format!("{major}/{minor}")),
158 }
159}
160
161fn system_uptime_ticks() -> Option<u64> {
162 let uptime = procfs::Uptime::current().ok()?;
163 let ticks_per_sec = procfs::ticks_per_second();
164 Some((uptime.uptime * ticks_per_sec as f64) as u64)
165}
166
167pub fn find_matching_processes(
182 opts: &MatchOptions,
183 tool_name: &str,
184) -> Result<Vec<ProcessInfo>, ExitCode> {
185 let pattern = &opts.pattern;
186
187 let regex_pattern = if opts.exact {
188 format!("^{pattern}$")
189 } else {
190 pattern.to_string()
191 };
192
193 let re = match regex::RegexBuilder::new(®ex_pattern)
194 .case_insensitive(opts.ignore_case)
195 .build()
196 {
197 Ok(re) => re,
198 Err(e) => {
199 eprintln!("{tool_name}: invalid pattern: {e}");
200 return Err(ExitCode::from(2));
201 }
202 };
203
204 let uid_filter: Option<Vec<u32>> = opts
205 .uid
206 .as_ref()
207 .map(|uids| uids.iter().filter_map(|u| resolve_uid(u)).collect());
208 let euid_filter: Option<Vec<u32>> = opts
209 .euid
210 .as_ref()
211 .map(|uids| uids.iter().filter_map(|u| resolve_uid(u)).collect());
212
213 let terminal_filter: Option<Vec<String>> =
214 opts.terminal.as_ref().map(|terms| {
215 terms
216 .iter()
217 .map(|t| t.strip_prefix("/dev/").unwrap_or(t).to_string())
218 .collect()
219 });
220
221 let env_spec: Option<EnvSpec> = opts.env.as_deref().map(parse_env_spec);
222
223 let my_pid = std::process::id() as i32;
224
225 let all_procs = match process::all_processes() {
226 Ok(iter) => iter,
227 Err(e) => {
228 eprintln!("{tool_name}: {e}");
229 return Err(ExitCode::from(3));
230 }
231 };
232
233 let uptime_ticks = system_uptime_ticks();
234
235 let mut matches: Vec<ProcessInfo> = Vec::new();
236
237 for proc_result in all_procs {
238 let proc = match proc_result {
239 Ok(p) => p,
240 Err(_) => continue,
241 };
242
243 if proc.pid() == my_pid {
244 continue;
245 }
246
247 let stat = match proc.stat() {
248 Ok(s) => s,
249 Err(_) => continue,
250 };
251
252 let cmdline_vec = proc.cmdline().unwrap_or_default();
253 let cmdline = cmdline_vec.join(" ");
254
255 let status = match proc.status() {
256 Ok(s) => s,
257 Err(_) => continue,
258 };
259
260 let info = ProcessInfo {
261 pid: stat.pid,
262 comm: stat.comm.clone(),
263 cmdline: if cmdline.is_empty() {
264 stat.comm.clone()
265 } else {
266 cmdline
267 },
268 euid: status.euid,
269 ruid: status.ruid,
270 rgid: status.rgid,
271 stat,
272 };
273
274 if let Some(ref pids) = opts.pid
275 && !pids.contains(&info.pid)
276 {
277 continue;
278 }
279
280 if let Some(ref parents) = opts.parent
281 && !parents.contains(&info.stat.ppid)
282 {
283 continue;
284 }
285
286 if let Some(ref pgroups) = opts.pgroup {
287 let pgrp = info.stat.pgrp;
288 if !pgroups.iter().any(|&pg| {
289 if pg == 0 {
290 pgrp == rustix::process::getpgrp().as_raw_nonzero().get()
291 } else {
292 pgrp == pg
293 }
294 }) {
295 continue;
296 }
297 }
298
299 if let Some(ref groups) = opts.group
300 && !groups.contains(&info.rgid)
301 {
302 continue;
303 }
304
305 if let Some(ref sessions) = opts.session {
306 let sess = info.stat.session;
307 if !sessions.iter().any(|&s| {
308 if s == 0 {
309 sess == rustix::process::getsid(None)
310 .map(|s| s.as_raw_nonzero().get())
311 .unwrap_or(0)
312 } else {
313 sess == s
314 }
315 }) {
316 continue;
317 }
318 }
319
320 if let Some(ref terms) = terminal_filter {
321 let proc_tty = tty_nr_to_name(info.stat.tty_nr);
322 match proc_tty {
323 None => continue,
324 Some(ref tty_name) => {
325 if !terms.iter().any(|t| tty_name == t) {
326 continue;
327 }
328 }
329 }
330 }
331
332 if let Some(ref uids) = uid_filter
333 && !uids.contains(&info.ruid)
334 {
335 continue;
336 }
337
338 if let Some(ref euids) = euid_filter
339 && !euids.contains(&info.euid)
340 {
341 continue;
342 }
343
344 if let Some(ref states) = opts.runstates
345 && !states.contains(&info.stat.state)
346 {
347 continue;
348 }
349
350 if let Some(ref spec) = env_spec
351 && !process_matches_env(&proc, spec)
352 {
353 continue;
354 }
355
356 if let Some(older_secs) = opts.older
357 && let Some(up_ticks) = uptime_ticks
358 {
359 let tps = procfs::ticks_per_second();
360 let age_secs = (up_ticks - info.stat.starttime) / tps;
361 if (age_secs as f64) < older_secs {
362 continue;
363 }
364 }
365
366 let text = info.match_text(opts.full);
367 let matched = if pattern.is_empty() {
368 true
369 } else {
370 re.is_match(text)
371 };
372
373 let matched = if opts.inverse { !matched } else { matched };
374
375 if matched {
376 matches.push(info);
377 }
378 }
379
380 matches.sort_by_key(|p| p.pid);
381
382 if opts.newest {
383 if let Some(newest) = matches.iter().max_by_key(|p| p.stat.starttime) {
384 let pid = newest.pid;
385 matches.retain(|p| p.pid == pid);
386 }
387 } else if opts.oldest
388 && let Some(oldest) = matches.iter().min_by_key(|p| p.stat.starttime)
389 {
390 let pid = oldest.pid;
391 matches.retain(|p| p.pid == pid);
392 }
393
394 Ok(matches)
395}