ytil_sys/
lsof.rs

1use std::path::PathBuf;
2use std::process::Command;
3use std::str::FromStr;
4
5use color_eyre::eyre::Context;
6use color_eyre::eyre::eyre;
7use itertools::Itertools as _;
8use ytil_cmd::CmdExt as _;
9
10#[derive(Debug)]
11pub enum ProcessFilter<'a> {
12    Pid(&'a str),
13    Name(&'a str),
14}
15
16#[derive(Debug)]
17pub struct ProcessDescription {
18    pub pid: String,
19    pub cwd: PathBuf,
20}
21
22/// Retrieves process descriptions using the lsof command.
23///
24/// # Errors
25/// - If the lsof command fails to execute.
26/// - If the lsof command exits with a non-zero status.
27/// - If the output cannot be parsed as UTF-8.
28/// - If parsing the lsof output fails.
29///
30/// # Assumptions
31/// - The `lsof` command is available on the system.
32///
33/// # Rationale
34/// Uses the `lsof` utility to query process information, specifically focusing on current working directories.
35/// The `-F n` format is used for machine-readable output, filtered by PID or command name.
36///
37/// # Performance
38/// Involves spawning an external process, so performance is IO-bound by the lsof execution time.
39pub fn lsof(process_filter: &ProcessFilter) -> color_eyre::Result<Vec<ProcessDescription>> {
40    let cmd = "lsof";
41
42    let process_filter = match process_filter {
43        ProcessFilter::Pid(pid) => ["-p", pid],
44        ProcessFilter::Name(name) => ["-c", name],
45    };
46    let mut args = vec!["-F", "n"];
47    args.extend(process_filter);
48    args.extend(["-a", "-d", "cwd"]);
49
50    let stdout = Command::new(cmd)
51        .args(&args)
52        .exec()
53        .wrap_err_with(|| eyre!("error running cmd | cmd={cmd:?} args={args:?}"))?
54        .exit_ok()
55        .wrap_err_with(|| eyre!("error cmd exit not ok | cmd={cmd:?} args={args:?}"))?
56        .stdout;
57
58    let output = str::from_utf8(&stdout)?;
59    parse_lsof_output(output)
60}
61
62fn parse_lsof_output(output: &str) -> color_eyre::Result<Vec<ProcessDescription>> {
63    let mut out = vec![];
64    // The hardcoded 3 is tight to the lsof args.
65    // Changes to lsof args will have impact on the chunks size.
66    for mut line in &output.lines().chunks(3) {
67        let pid = line
68            .next()
69            .ok_or_else(|| eyre!("error missing pid in lsof line"))?
70            .trim_start_matches('p');
71        line.next().ok_or_else(|| eyre!("error missing f in lsof line"))?;
72        let cwd = line
73            .next()
74            .ok_or_else(|| eyre!("error missing cwd in lsof line"))?
75            .trim_start_matches('n');
76
77        out.push(ProcessDescription {
78            pid: pid.to_owned(),
79            cwd: PathBuf::from_str(cwd)
80                .wrap_err_with(|| format!("error constructing PathBuf from cwd | cwd={cwd:?}"))?,
81        });
82    }
83    Ok(out)
84}