nu_system/
linux.rs

1use log::info;
2use procfs::process::{FDInfo, Io, Process, Stat, Status};
3use procfs::{ProcError, ProcessCGroups, WithCurrentSystemInfo};
4use std::path::PathBuf;
5use std::thread;
6use std::time::{Duration, Instant};
7
8pub enum ProcessTask {
9    Process(Process),
10    Task { stat: Box<Stat>, owner: u32 },
11}
12
13impl ProcessTask {
14    pub fn stat(&self) -> Result<Stat, ProcError> {
15        match self {
16            ProcessTask::Process(x) => x.stat(),
17            ProcessTask::Task { stat: x, owner: _ } => Ok(*x.clone()),
18        }
19    }
20
21    pub fn cmdline(&self) -> Result<Vec<String>, ProcError> {
22        match self {
23            ProcessTask::Process(x) => x.cmdline(),
24            _ => Err(ProcError::Other("not supported".to_string())),
25        }
26    }
27
28    pub fn cgroups(&self) -> Result<ProcessCGroups, ProcError> {
29        match self {
30            ProcessTask::Process(x) => x.cgroups(),
31            _ => Err(ProcError::Other("not supported".to_string())),
32        }
33    }
34
35    pub fn fd(&self) -> Result<Vec<FDInfo>, ProcError> {
36        match self {
37            ProcessTask::Process(x) => x.fd()?.collect(),
38            _ => Err(ProcError::Other("not supported".to_string())),
39        }
40    }
41
42    pub fn loginuid(&self) -> Result<u32, ProcError> {
43        match self {
44            ProcessTask::Process(x) => x.loginuid(),
45            _ => Err(ProcError::Other("not supported".to_string())),
46        }
47    }
48
49    pub fn owner(&self) -> u32 {
50        match self {
51            ProcessTask::Process(x) => x.uid().unwrap_or(0),
52            ProcessTask::Task { stat: _, owner: x } => *x,
53        }
54    }
55
56    pub fn wchan(&self) -> Result<String, ProcError> {
57        match self {
58            ProcessTask::Process(x) => x.wchan(),
59            _ => Err(ProcError::Other("not supported".to_string())),
60        }
61    }
62}
63
64pub struct ProcessInfo {
65    pub pid: i32,
66    pub ppid: i32,
67    pub curr_proc: ProcessTask,
68    pub curr_io: Option<Io>,
69    pub prev_io: Option<Io>,
70    pub curr_stat: Option<Stat>,
71    pub prev_stat: Option<Stat>,
72    pub curr_status: Option<Status>,
73    pub interval: Duration,
74    pub cwd: PathBuf,
75}
76
77pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec<ProcessInfo> {
78    let mut base_procs = Vec::new();
79    let mut ret = Vec::new();
80
81    // Take an initial snapshot of process I/O and CPU info, so we can calculate changes over time
82    if let Ok(all_proc) = procfs::process::all_processes() {
83        for proc in all_proc.flatten() {
84            let io = proc.io().ok();
85            let stat = proc.stat().ok();
86            let time = Instant::now();
87            base_procs.push((proc.pid(), io, stat, time));
88        }
89    }
90
91    // wait a bit...
92    thread::sleep(interval);
93
94    // now get process info again, build up results
95    for (pid, prev_io, prev_stat, prev_time) in base_procs {
96        let curr_proc_pid = pid;
97        let curr_proc = if let Ok(p) = Process::new(curr_proc_pid) {
98            p
99        } else {
100            info!(
101                "failed to retrieve info for pid={curr_proc_pid}, process probably died between snapshots"
102            );
103            continue;
104        };
105        let cwd = curr_proc.cwd().unwrap_or_default();
106
107        let curr_io = curr_proc.io().ok();
108        let curr_stat = curr_proc.stat().ok();
109        let curr_status = curr_proc.status().ok();
110        let curr_time = Instant::now();
111        let interval = curr_time.saturating_duration_since(prev_time);
112        let ppid = curr_proc.stat().map(|p| p.ppid).unwrap_or_default();
113        let curr_proc = ProcessTask::Process(curr_proc);
114
115        let proc = ProcessInfo {
116            pid,
117            ppid,
118            curr_proc,
119            curr_io,
120            prev_io,
121            curr_stat,
122            prev_stat,
123            curr_status,
124            interval,
125            cwd,
126        };
127
128        ret.push(proc);
129    }
130
131    ret
132}
133
134impl ProcessInfo {
135    /// PID of process
136    pub fn pid(&self) -> i32 {
137        self.pid
138    }
139
140    /// PPID of process
141    pub fn ppid(&self) -> i32 {
142        self.ppid
143    }
144
145    /// Name of command
146    pub fn name(&self) -> String {
147        if let Some(name) = self.comm() {
148            return name;
149        }
150        // Fall back in case /proc/<pid>/stat source is not available.
151        if let Ok(mut cmd) = self.curr_proc.cmdline() {
152            if let Some(name) = cmd.first_mut() {
153                // Take over the first element and return it without extra allocations
154                // (String::default() is allocation-free).
155                return std::mem::take(name);
156            }
157        }
158        String::new()
159    }
160
161    /// Full name of command, with arguments
162    ///
163    /// WARNING: As this does no escaping, this function is lossy. It's OK-ish for display purposes
164    /// but nothing else.
165    // TODO: Maybe rename this to display_command and add escaping compatible with nushell?
166    pub fn command(&self) -> String {
167        if let Ok(cmd) = self.curr_proc.cmdline() {
168            // Things like kworker/0:0 still have the cmdline file in proc, even though it's empty.
169            if !cmd.is_empty() {
170                return cmd.join(" ").replace(['\n', '\t'], " ");
171            }
172        }
173        self.comm().unwrap_or_default()
174    }
175
176    pub fn cwd(&self) -> String {
177        self.cwd.display().to_string()
178    }
179
180    /// Get the status of the process
181    pub fn status(&self) -> String {
182        if let Ok(p) = self.curr_proc.stat() {
183            match p.state {
184                'S' => "Sleeping",
185                'R' => "Running",
186                'D' => "Disk sleep",
187                'Z' => "Zombie",
188                'T' => "Stopped",
189                't' => "Tracing",
190                'X' => "Dead",
191                'x' => "Dead",
192                'K' => "Wakekill",
193                'W' => "Waking",
194                'P' => "Parked",
195                _ => "Unknown",
196            }
197        } else {
198            "Unknown"
199        }
200        .into()
201    }
202
203    /// CPU usage as a percent of total
204    pub fn cpu_usage(&self) -> f64 {
205        if let Some(cs) = &self.curr_stat {
206            if let Some(ps) = &self.prev_stat {
207                let curr_time = cs.utime + cs.stime;
208                let prev_time = ps.utime + ps.stime;
209
210                let usage_ms =
211                    curr_time.saturating_sub(prev_time) * 1000 / procfs::ticks_per_second();
212                let interval_ms =
213                    self.interval.as_secs() * 1000 + u64::from(self.interval.subsec_millis());
214                usage_ms as f64 * 100.0 / interval_ms as f64
215            } else {
216                0.0
217            }
218        } else {
219            0.0
220        }
221    }
222
223    /// Memory size in number of bytes
224    pub fn mem_size(&self) -> u64 {
225        match self.curr_proc.stat() {
226            Ok(p) => p.rss_bytes().get(),
227            Err(_) => 0,
228        }
229    }
230
231    /// Virtual memory size in bytes
232    pub fn virtual_size(&self) -> u64 {
233        self.curr_proc.stat().map(|p| p.vsize).unwrap_or_default()
234    }
235
236    fn comm(&self) -> Option<String> {
237        self.curr_proc.stat().map(|st| st.comm).ok()
238    }
239}