Skip to main content

nu_system/
linux.rs

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