mirror of
https://github.com/nushell/nushell.git
synced 2026-10-28 08:54:52 +08:00
fix(ps): list every process and report the same columns on every platform (#19145)
## Description This PR refactors the `ps` command and consolidates some code with `nu_system::ProcessInfo` while also making it more portable having the same columns on all operating systems with `ps` and `ps -l`. It also fixes some bugs where all the processes weren't being seen on MacOS. Also there was some debate as to whether 0 was valid for cpu. `cpu` is the percent of one core used during a ~100ms sample, like `top`. Most processes are idle during any 100ms, and `/bin/ps` agrees. Per-process values won't always match `/bin/ps`: macOS's `%CPU` is the kernel's decaying recent average (a process stopped after 3 seconds at 100% shows 0.0 five seconds later), and Linux's `ps` shows lifetime average CPU. ## User-facing changes (Release notes) - `ps` now lists every process on macOS (zombies included) and Windows (all but the System Idle Process), including other users' and protected processes. Values the operating system doesn't make available for a process (on macOS, the CPU, memory and threads of other users' processes unless you run as root) are null instead of the process being left out. - `ps` and `ps -l` now return the same columns on every platform. A column is null when the platform or the process doesn't provide that value. - `ps`: `pid ppid name user status cpu cpu_time mem` - `ps -l` adds: `virtual private command exe start_time user_id process_group_id session_id priority nice threads read written cwd environment` - New columns: - `cpu_time`: total CPU time the process has used. - `private`: the memory only the process uses, the number Activity Monitor and Task Manager show as "Memory". - `nice`: the nice value; on Windows, the priority class on the nice scale. - `read` and `written`: bytes of storage I/O since the process started (all I/O on Windows). - `exe`: the path of the executable. - Also new on the platforms that were missing them: `user`, `user_id`, `process_group_id`, `session_id`, `threads`, `cwd` and `environment`, and `start_time` on the BSDs. - Changed columns: - `user` is now in `ps`, and `virtual` is now only in `ps -l`. - `process_threads` is renamed to `threads`. - `working` (always the same as `mem`) and `paged` are removed. Use `private` for the memory a process uses by itself. - Linux: - `cpu` is now measured to the nanosecond instead of in 10% steps. - `name` is no longer cut to 15 characters when argv[0] or the executable shows the full name. - `cwd` and `environment` are null for processes you can't read (other users' processes unless you are root); `cwd` used to be an empty string. - Windows: - `ps` now has a `status` column (`Running`, `Sleeping` or `Suspended`). - `user_sid` is replaced by `user_id`, which holds the SID string (for example `S-1-5-18`). - `virtual` is now the virtual address space size, as on other platforms. - `priority` is the base priority (8 for normal priority). - `nice` is the priority class on the nice scale, as libuv and Node.js map it (normal 0, high -14, idle 19). - `process_group_id` is the console process group, the one `CREATE_NEW_PROCESS_GROUP` starts and `GenerateConsoleCtrlEvent` sends Ctrl+Break to. - macOS: - `cwd` is now the current working directory rather than the directory the process started in. - `status` now uses `Sleeping` instead of `Sleep` and `Waiting`, matching the other platforms, and zombies are listed as `Zombie`. - `kernel_task` (pid 0) is now listed, as Activity Monitor does. - FreeBSD, NetBSD and OpenBSD: - Zombies and other processes whose arguments can't be read are now listed, under the kernel's process name. - On FreeBSD, `cpu` now includes threads that start or exit during the sample. - On OpenBSD, `virtual` is no longer always 0. - `command` shows newlines and tabs in arguments as spaces on Windows and the BSDs too, as it already did on Linux and macOS. - Null sorts after every number, so the examples now drop nulls first: `ps | compact cpu | sort-by cpu | last 3`. Matching a text column that can be null with `=~` is an error, so also drop those rows first: `ps | compact status | where status =~ Run`. ## Additional notes closes #18358 Let this serve as documentation about what the columns mean per platform. The column set follows what top, htop, btop, btm, procs, Task Manager and Activity Monitor show: | column | Linux | macOS | Windows | FreeBSD | NetBSD / OpenBSD | | --- | --- | --- | --- | --- | --- | | `user` | name of the effective uid | same | account of the token's SID | same as Linux | same as Linux | | `status` | `stat` state | thread states; zombie/stopped for all | thread states | `ki_stat` | `p_stat` | | `cpu` | process CPU-time clock (ns), or `stat` ticks | task times | process times | `ki_runtime` | `p_rtime` | | `cpu_time` | `utime + stime` | task times | user + kernel time | `ki_runtime` | `p_rtime` (null for zombies) | | `mem` | RSS | resident size | working set | RSS | RSS | | `virtual` | VSZ | virtual size | virtual size | VSZ | VSZ | | `private` | `RssAnon` | physical footprint (own processes) | private working set | resident pages of anonymous mappings from `KERN_PROC_VMMAP` (own processes) | null: no private-memory figure | | `exe` | `/proc/<pid>/exe` (own processes) | `proc_pidpath` | `QueryFullProcessImageNameW` | `KERN_PROC_PATHNAME` | `KERN_PROC_PATHNAME`; null on OpenBSD, which doesn't expose it | | `start_time` | `stat` | `kinfo_proc` | creation time | `ki_start` | `p_ustart` (null for zombies) | | `user_id` | effective uid | effective uid | SID string | `ki_uid` | `p_uid` | | `process_group_id` | `stat` | short BSD info | console process group from the process parameters | `ki_pgid` | `p__pgid` | | `session_id` | `stat` | `getsid` (null for kernel_task) | Terminal Services session | `ki_sid` | `p_sid` | | `priority` | kernel priority | task priority (own processes) | base priority | `ki_pri.pri_level` | `p_priority` | | `nice` | `stat` | `kinfo_proc` | priority class on the nice scale | `ki_nice` | `p_nice` | | `threads` | `stat` | task info (own processes) | snapshot | `ki_numthreads` | `p_nlwps`; null on OpenBSD, whose `kinfo_proc` has no thread count | | `read` / `written` | storage bytes from `/proc/<pid>/io` (own processes) | storage bytes from `proc_pid_rusage` (own processes) | all I/O bytes, including pipes and network | null: the kernel counts block operations, not bytes | null: the kernel counts block operations, not bytes | | `cwd` | `/proc/<pid>/cwd` (own processes) | `PROC_PIDVNODEPATHINFO` (own processes) | process parameters | `KERN_PROC_CWD` (own processes) | `KERN_PROC_CWD` (own processes) | | `environment` | `/proc/<pid>/environ` (own processes) | `KERN_PROCARGS2` (own processes) | process parameters | `KERN_PROC_ENV` (own processes) | `KERN_PROC_ENV` (own processes) | "Own processes" means the current user's processes, or every process when running as root. Every backend reads each column from the kernel. These have no kernel interface to read from and stay null, with the reason in a comment where the backend sets them: `read`/`written` on the BSDs, `private` on NetBSD and OpenBSD, and `exe` and `threads` on OpenBSD.
This commit is contained in:
1 parent
f92634ceb7
commit
b6671f5cdc
18 files changed
+1594
-1562
No files matched your search
Generated
+2
-4
@@ -3970,9 +3970,9 @@ checksum = "803ec87c9cfb29b9d2633f20cba1f488db3fd53f2158b1024cbefb47ba05d413"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "libflate"
|
||||
@@ -4989,7 +4989,6 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pretty_assertions",
|
||||
"print-positions",
|
||||
"procfs",
|
||||
"pure-rust-locales",
|
||||
"quick-xml 0.42.0",
|
||||
"rand 0.10.2",
|
||||
@@ -5432,7 +5431,6 @@ dependencies = [
|
||||
name = "nu-system"
|
||||
version = "0.116.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"itertools 0.15.0",
|
||||
"libc",
|
||||
"libproc",
|
||||
|
||||
+1
-1
@@ -186,7 +186,7 @@ itertools = "0.15"
|
||||
kitest = "0.6.0"
|
||||
lean_string = { version = "0.7.0", features = ["serde"] }
|
||||
lexopt = "0.3.2"
|
||||
libc = "0.2.185"
|
||||
libc = "0.2.189"
|
||||
libproc = "0.14.11"
|
||||
log = "0.4.34"
|
||||
lru = "0.18.3"
|
||||
|
||||
@@ -125,13 +125,13 @@ For example, we could use the built-in `ps` command to get a list of the running
|
||||
|
||||
```shell
|
||||
ps | where cpu > 0
|
||||
# => ╭───┬───────┬───────────┬───────┬───────────┬───────────╮
|
||||
# => │ # │ pid │ name │ cpu │ mem │ virtual │
|
||||
# => ├───┼───────┼───────────┼───────┼───────────┼───────────┤
|
||||
# => │ 0 │ 2240 │ Slack.exe │ 16.40 │ 178.3 MiB │ 232.6 MiB │
|
||||
# => │ 1 │ 16948 │ Slack.exe │ 16.32 │ 205.0 MiB │ 197.9 MiB │
|
||||
# => │ 2 │ 17700 │ nu.exe │ 3.77 │ 26.1 MiB │ 8.8 MiB │
|
||||
# => ╰───┴───────┴───────────┴───────┴───────────┴───────────╯
|
||||
# => ╭───┬───────┬──────┬───────────┬──────┬─────────┬───────┬────────────┬──────────╮
|
||||
# => │ # │ pid │ ppid │ name │ user │ status │ cpu │ cpu_time │ mem │
|
||||
# => ├───┼───────┼──────┼───────────┼──────┼─────────┼───────┼────────────┼──────────┤
|
||||
# => │ 0 │ 2240 │ 9872 │ Slack.exe │ dev │ Running │ 16.40 │ 1min 12sec │ 178.3 MB │
|
||||
# => │ 1 │ 16948 │ 9872 │ Slack.exe │ dev │ Running │ 16.32 │ 48sec │ 205.0 MB │
|
||||
# => │ 2 │ 17700 │ 4632 │ nu.exe │ dev │ Running │ 3.77 │ 2sec │ 26.1 MB │
|
||||
# => ╰───┴───────┴──────┴───────────┴──────┴─────────┴───────┴────────────┴──────────╯
|
||||
```
|
||||
|
||||
### Opening files
|
||||
|
||||
@@ -179,9 +179,6 @@ nix = { workspace = true, default-features = false, features = [
|
||||
"pthread",
|
||||
] }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||
procfs = { workspace = true }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies.trash]
|
||||
optional = true
|
||||
workspace = true
|
||||
|
||||
@@ -152,7 +152,7 @@ impl Command for Kill {
|
||||
vec![
|
||||
Example {
|
||||
description: "Kill the pid using the most memory.",
|
||||
example: "ps | sort-by mem | last | kill $in.pid",
|
||||
example: "ps | compact mem | sort-by mem | last | kill $in.pid",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use chrono::{Local, TimeZone};
|
||||
#[cfg(windows)]
|
||||
use itertools::Itertools;
|
||||
use chrono::{DateTime, Local};
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
use nu_protocol::PipelineMetadata;
|
||||
#[cfg(target_os = "linux")]
|
||||
use procfs::WithCurrentSystemInfo;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -33,6 +28,14 @@ impl Command for Ps {
|
||||
"View information about system processes."
|
||||
}
|
||||
|
||||
fn extra_description(&self) -> &str {
|
||||
"The columns are the same on every platform. `cpu` is the percent of one CPU core the process used during a short (about 100ms) sample, like `top` shows, and `cpu_time` is the total CPU time it has used since it started. `mem` is resident memory.
|
||||
|
||||
With `--long`, `virtual` is the size of the process's virtual address space and `private` is the memory only it uses, the number Activity Monitor and Task Manager show as \"Memory\". `read` and `written` count bytes of storage I/O since the process started; on Windows they count all I/O, including pipes and network. `user_id` is the numeric user id on unix and the SID string (for example `S-1-5-18`) on Windows. Windows has priority classes rather than nice values, so its `nice` is the priority class on the nice scale, the way libuv and Node.js map it, and its `process_group_id` is the console process group.
|
||||
|
||||
A column is null when the operating system doesn't make that value available for a process: for example, macOS only shows the CPU, memory, threads, environment and working directory of other users' processes to root (their `command` is the executable's path), and the BSDs count disk I/O in blocks rather than bytes. Null sorts after every number and can't be matched with `=~`, so use `compact` to drop those rows before sorting or matching text."
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec![
|
||||
"procedures",
|
||||
@@ -63,12 +66,17 @@ impl Command for Ps {
|
||||
},
|
||||
Example {
|
||||
description: "List the top 5 system processes with the highest memory usage",
|
||||
example: "ps | sort-by mem | last 5",
|
||||
example: "ps | compact mem | sort-by mem | last 5",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "List the top 3 system processes with the highest CPU usage",
|
||||
example: "ps | sort-by cpu | last 3",
|
||||
example: "ps | compact cpu | sort-by cpu | last 3",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "List the 3 system processes that have used the most CPU time",
|
||||
example: "ps | compact cpu_time | sort-by cpu_time | last 3",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
@@ -90,109 +98,55 @@ fn run_ps(
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let mut output = vec![];
|
||||
let span = call.head;
|
||||
let long = call.has_flag(engine_state, stack, "long")?;
|
||||
let filesize = |bytes: Option<u64>| {
|
||||
bytes.map_or(Value::nothing(span), |bytes| {
|
||||
Value::filesize(bytes as i64, span)
|
||||
})
|
||||
};
|
||||
|
||||
for proc in nu_system::collect_proc(Duration::from_millis(100), false) {
|
||||
let mut record = Record::new();
|
||||
let output: Vec<Value> = nu_system::collect_proc(Duration::from_millis(100), long)
|
||||
.into_iter()
|
||||
.map(|proc| {
|
||||
let mut record = Record::new();
|
||||
|
||||
record.push("pid", Value::int(proc.pid() as i64, span));
|
||||
record.push("ppid", Value::int(proc.ppid() as i64, span));
|
||||
record.push("name", Value::string(proc.name(), span));
|
||||
record.push("pid", Value::int(proc.pid() as i64, span));
|
||||
record.push("ppid", Value::int(proc.ppid() as i64, span));
|
||||
record.push("name", Value::string(proc.name(), span));
|
||||
record.push("user", proc.user().into_value(span));
|
||||
record.push("status", proc.status().into_value(span));
|
||||
record.push("cpu", proc.cpu_usage().into_value(span));
|
||||
record.push("cpu_time", proc.cpu_time().into_value(span));
|
||||
record.push("mem", filesize(proc.mem_size()));
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
// Hide status on Windows until we can find a good way to support it
|
||||
record.push("status", Value::string(proc.status(), span));
|
||||
}
|
||||
|
||||
record.push("cpu", Value::float(proc.cpu_usage(), span));
|
||||
record.push("mem", Value::filesize(proc.mem_size() as i64, span));
|
||||
record.push("virtual", Value::filesize(proc.virtual_size() as i64, span));
|
||||
|
||||
if long {
|
||||
record.push("command", Value::string(proc.command(), span));
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let Ok(proc_stat) = proc.curr_proc.stat() else {
|
||||
continue;
|
||||
};
|
||||
if long {
|
||||
record.push("virtual", filesize(proc.virtual_size()));
|
||||
record.push("private", filesize(proc.private_size()));
|
||||
record.push("command", Value::string(proc.command(), span));
|
||||
record.push("exe", proc.exe().into_value(span));
|
||||
record.push(
|
||||
"start_time",
|
||||
match proc_stat.starttime().get() {
|
||||
Ok(ts) => Value::date(ts.into(), span),
|
||||
Err(_) => Value::nothing(span),
|
||||
},
|
||||
proc.start_time()
|
||||
.map(|time| DateTime::<Local>::from(time).fixed_offset())
|
||||
.into_value(span),
|
||||
);
|
||||
record.push("user_id", Value::int(proc.curr_proc.owner() as i64, span));
|
||||
record.push("process_group_id", Value::int(proc_stat.pgrp as i64, span));
|
||||
record.push("session_id", Value::int(proc_stat.session as i64, span));
|
||||
// This may be helpful for ctrl+z type of checking, once we get there
|
||||
// record.push("tpg_id", Value::int(proc_stat.tpgid as i64, span));
|
||||
record.push("priority", Value::int(proc_stat.priority, span));
|
||||
record.push("process_threads", Value::int(proc_stat.num_threads, span));
|
||||
record.push("working", Value::filesize(proc.working_size() as i64, span));
|
||||
record.push("paged", Value::filesize(proc.paged_size() as i64, span));
|
||||
record.push("cwd", Value::string(proc.cwd(), span));
|
||||
// A uid on unix, a SID string such as "S-1-5-18" on Windows.
|
||||
record.push("user_id", proc.user_id().into_value(span));
|
||||
record.push("process_group_id", proc.process_group_id().into_value(span));
|
||||
record.push("session_id", proc.session_id().into_value(span));
|
||||
record.push("priority", proc.priority().into_value(span));
|
||||
record.push("nice", proc.nice().into_value(span));
|
||||
record.push("threads", proc.thread_count().into_value(span));
|
||||
record.push("read", filesize(proc.disk_read()));
|
||||
record.push("written", filesize(proc.disk_written()));
|
||||
record.push("cwd", proc.cwd().into_value(span));
|
||||
record.push("environment", proc.environ().into_value(span));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
//TODO: There's still more information we can cram in there if we want to
|
||||
// see the ProcessInfo struct for more information
|
||||
record.push(
|
||||
"start_time",
|
||||
Value::date(proc.start_time.fixed_offset(), span),
|
||||
);
|
||||
record.push(
|
||||
"user",
|
||||
Value::string(
|
||||
proc.user.clone().name.unwrap_or("unknown".to_string()),
|
||||
span,
|
||||
),
|
||||
);
|
||||
record.push(
|
||||
"user_sid",
|
||||
Value::string(
|
||||
proc.user
|
||||
.clone()
|
||||
.sid
|
||||
.iter()
|
||||
.map(|r| r.to_string())
|
||||
.join("-"),
|
||||
span,
|
||||
),
|
||||
);
|
||||
record.push("priority", Value::int(proc.priority as i64, span));
|
||||
record.push("working", Value::filesize(proc.working_size() as i64, span));
|
||||
record.push("paged", Value::filesize(proc.paged_size() as i64, span));
|
||||
record.push("cwd", Value::string(proc.cwd(), span));
|
||||
record.push(
|
||||
"environment",
|
||||
Value::list(
|
||||
proc.environ()
|
||||
.iter()
|
||||
.map(|x| Value::string(x.to_string(), span))
|
||||
.collect(),
|
||||
span,
|
||||
),
|
||||
);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let timestamp = Local
|
||||
.timestamp_nanos(proc.start_time * 1_000_000_000)
|
||||
.into();
|
||||
record.push("start_time", Value::date(timestamp, span));
|
||||
record.push("user_id", Value::int(proc.user_id, span));
|
||||
record.push("priority", Value::int(proc.priority, span));
|
||||
record.push("process_threads", Value::int(proc.task_thread_num, span));
|
||||
record.push("cwd", Value::string(proc.cwd(), span));
|
||||
}
|
||||
}
|
||||
|
||||
output.push(Value::record(record, span));
|
||||
}
|
||||
Value::record(record, span)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(output.into_pipeline_data_with_metadata(
|
||||
span,
|
||||
|
||||
@@ -425,10 +425,6 @@ fn exiting_nushell_kills_jobs() -> Result {
|
||||
#[test]
|
||||
#[serial]
|
||||
#[deps(NU)]
|
||||
#[cfg_attr(
|
||||
target_os = "macos",
|
||||
ignore = "`process_group_id` not available in `ps -l`"
|
||||
)]
|
||||
fn jobs_get_group_id_right() -> Result {
|
||||
let code = r#"
|
||||
let job1 = job spawn { nu -c "sleep 0.5sec" | nu -c "sleep 0.5sec"; }
|
||||
|
||||
@@ -760,7 +760,7 @@ $env.config.abbreviations = {}
|
||||
# $env.config.abbreviations = {
|
||||
# gs: "git status",
|
||||
# ll: "ls -l",
|
||||
# ptop: "ps | sort-by -r cpu | first 10"
|
||||
# ptop: "ps | compact cpu | sort-by -r cpu | first 10"
|
||||
# }
|
||||
|
||||
# -----
|
||||
|
||||
@@ -39,18 +39,16 @@ libproc = { workspace = true }
|
||||
mach2 = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
chrono = { workspace = true, default-features = false, features = ["clock"] }
|
||||
ntapi = "0.4"
|
||||
windows = { workspace = true, features = [
|
||||
"Wdk_System_SystemServices",
|
||||
"Wdk_System_Threading",
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Authorization",
|
||||
"Win32_System_Diagnostics_Debug",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_System_Kernel",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_ProcessStatus",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Shell",
|
||||
|
||||
@@ -11,16 +11,18 @@ fn main() {
|
||||
.unwrap_or(1);
|
||||
for run in 1..=10 {
|
||||
for proc in nu_system::collect_proc(std::time::Duration::from_millis(100), false) {
|
||||
if proc.cpu_usage() > 0.00001 {
|
||||
if let Some(cpu) = proc.cpu_usage()
|
||||
&& cpu > 0.00001
|
||||
{
|
||||
println!(
|
||||
"{} - {} - {} - {} - {:.2}% - {}M - {}M - {} procs",
|
||||
run,
|
||||
proc.pid(),
|
||||
proc.name(),
|
||||
proc.status(),
|
||||
proc.cpu_usage() / cores as f64,
|
||||
proc.mem_size() / (1024 * 1024),
|
||||
proc.virtual_size() / (1024 * 1024),
|
||||
proc.status().unwrap_or("Unknown"),
|
||||
cpu / cores as f64,
|
||||
proc.mem_size().unwrap_or_default() / (1024 * 1024),
|
||||
proc.virtual_size().unwrap_or_default() / (1024 * 1024),
|
||||
cores,
|
||||
)
|
||||
}
|
||||
|
||||
+134
-155
@@ -1,38 +1,33 @@
|
||||
use itertools::{EitherOrBoth, Itertools};
|
||||
use libc::{
|
||||
CTL_HW, CTL_KERN, KERN_PROC, KERN_PROC_ALL, KERN_PROC_ARGS, TDF_IDLETD, c_char, kinfo_proc,
|
||||
sysctl,
|
||||
CTL_HW, CTL_KERN, KERN_PROC, KERN_PROC_ARGS, KERN_PROC_CWD, KERN_PROC_ENV, KERN_PROC_PATHNAME,
|
||||
KERN_PROC_PROC, KERN_PROC_VMMAP, KVME_TYPE_DEFAULT, KVME_TYPE_SWAP, TDF_IDLETD, c_char,
|
||||
kinfo_file, kinfo_vmentry, sysctl,
|
||||
};
|
||||
use std::{
|
||||
ffi::CStr,
|
||||
io,
|
||||
mem::{self, MaybeUninit},
|
||||
ptr,
|
||||
time::Duration,
|
||||
time::{Duration, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use crate::process::{ProcessInfo, name_and_command};
|
||||
use crate::unix::{UserNames, c_string, split_nul};
|
||||
use nu_utils::time::Instant;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProcessInfo {
|
||||
pub pid: i32,
|
||||
pub ppid: i32,
|
||||
pub name: String,
|
||||
pub argv: Vec<u8>,
|
||||
pub stat: c_char,
|
||||
pub percent_cpu: f64,
|
||||
pub mem_resident: u64, // in bytes
|
||||
pub mem_virtual: u64, // in bytes
|
||||
}
|
||||
|
||||
pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec<ProcessInfo> {
|
||||
compare_procs(interval).unwrap_or_else(|err| {
|
||||
/// Lists every process, measuring CPU usage over `interval`. When `long` is false, the details
|
||||
/// only `ps --long` shows aren't read, so they are `None`.
|
||||
///
|
||||
/// The working directory and environment are only readable for the current user's processes (or
|
||||
/// as root).
|
||||
pub fn collect_proc(interval: Duration, long: bool) -> Vec<ProcessInfo> {
|
||||
compare_procs(interval, long).unwrap_or_else(|err| {
|
||||
log::warn!("Failed to get processes: {}", err);
|
||||
vec![]
|
||||
})
|
||||
}
|
||||
|
||||
fn compare_procs(interval: Duration) -> io::Result<Vec<ProcessInfo>> {
|
||||
fn compare_procs(interval: Duration, long: bool) -> io::Result<Vec<ProcessInfo>> {
|
||||
let pagesize = get_pagesize()? as u64;
|
||||
|
||||
// Compare two full snapshots of all of the processes over the interval
|
||||
@@ -43,74 +38,87 @@ fn compare_procs(interval: Duration) -> io::Result<Vec<ProcessInfo>> {
|
||||
let true_interval = Instant::now().saturating_duration_since(now);
|
||||
let true_interval_sec = true_interval.as_secs_f64();
|
||||
|
||||
// Group all of the threads in each process together
|
||||
let a_grouped = procs_a.into_iter().group_by(|proc| proc.ki_pid);
|
||||
let b_grouped = procs_b.into_iter().group_by(|proc| proc.ki_pid);
|
||||
let mut user_names = UserNames::default();
|
||||
|
||||
// Join the processes between the two snapshots
|
||||
Ok(a_grouped
|
||||
Ok(procs_a
|
||||
.into_iter()
|
||||
.merge_join_by(b_grouped.into_iter(), |(pid_a, _), (pid_b, _)| {
|
||||
pid_a.cmp(pid_b)
|
||||
})
|
||||
.map(|threads| {
|
||||
// Join the threads between the two snapshots for the process
|
||||
let mut threads = {
|
||||
let (left, right) = threads.left_and_right();
|
||||
left.into_iter()
|
||||
.flat_map(|(_, threads)| threads)
|
||||
.merge_join_by(
|
||||
right.into_iter().flat_map(|(_, threads)| threads),
|
||||
|thread_a, thread_b| thread_a.ki_tid.cmp(&thread_b.ki_tid),
|
||||
)
|
||||
.peekable()
|
||||
.merge_join_by(procs_b, |a, b| a.ki_pid.cmp(&b.ki_pid))
|
||||
.filter_map(|procs| {
|
||||
let (prev_proc, proc) = match procs {
|
||||
EitherOrBoth::Both(a, b) => (Some(a), b),
|
||||
// Started during the sample, so there's nothing to compare against.
|
||||
EitherOrBoth::Right(b) => (None, b),
|
||||
// Exited during the sample.
|
||||
EitherOrBoth::Left(_) => return None,
|
||||
};
|
||||
|
||||
// Pick the later process entry of the first thread to use for basic process information
|
||||
let proc = match threads.peek().ok_or(io::ErrorKind::NotFound)? {
|
||||
EitherOrBoth::Both(_, b) => b,
|
||||
EitherOrBoth::Left(a) => a,
|
||||
EitherOrBoth::Right(b) => b,
|
||||
}
|
||||
.clone();
|
||||
|
||||
// Skip over the idle process. It always appears with high CPU usage when the
|
||||
// system is idle
|
||||
if proc.ki_tdflags as u64 & TDF_IDLETD as u64 != 0 {
|
||||
return Err(io::ErrorKind::NotFound.into());
|
||||
return None;
|
||||
}
|
||||
|
||||
// Aggregate all of the threads that exist in both snapshots and sum their runtime.
|
||||
let (runtime_a, runtime_b) =
|
||||
threads
|
||||
.flat_map(|t| t.both())
|
||||
.fold((0., 0.), |(runtime_a, runtime_b), (a, b)| {
|
||||
let runtime_in_seconds =
|
||||
|proc: &kinfo_proc| proc.ki_runtime as f64 /* µsec */ / 1_000_000.0;
|
||||
(
|
||||
runtime_a + runtime_in_seconds(&a),
|
||||
runtime_b + runtime_in_seconds(&b),
|
||||
)
|
||||
});
|
||||
|
||||
// The percentage CPU is the ratio of how much runtime occurred for the process out of
|
||||
// the true measured interval that occurred.
|
||||
let percent_cpu = 100. * (runtime_b - runtime_a).max(0.) / true_interval_sec;
|
||||
// the true measured interval that occurred. `ki_runtime` is the process total in
|
||||
// microseconds, including threads that have exited. A different start time means
|
||||
// the pid was reused.
|
||||
let start = (proc.ki_start.tv_sec, proc.ki_start.tv_usec);
|
||||
let percent_cpu = prev_proc
|
||||
.filter(|prev| (prev.ki_start.tv_sec, prev.ki_start.tv_usec) == start)
|
||||
.map(|prev| {
|
||||
let used = proc.ki_runtime.saturating_sub(prev.ki_runtime);
|
||||
100. * used as f64 / 1_000_000. / true_interval_sec
|
||||
});
|
||||
|
||||
let info = ProcessInfo {
|
||||
#[allow(clippy::unnecessary_cast, reason = "`c_char` is `u8` on some targets")]
|
||||
let comm = proc.ki_comm.map(|c| c as u8);
|
||||
// Keep the process even when its arguments can't be read (a zombie, say).
|
||||
let (name, command) = name_and_command(
|
||||
&proc_sysctl(KERN_PROC_ARGS, proc.ki_pid).unwrap_or_default(),
|
||||
c_string(&comm).unwrap_or_default(),
|
||||
);
|
||||
|
||||
Some(ProcessInfo {
|
||||
pid: proc.ki_pid,
|
||||
ppid: proc.ki_ppid,
|
||||
name: read_cstr(&proc.ki_comm).to_string_lossy().into_owned(),
|
||||
argv: get_proc_args(proc.ki_pid)?,
|
||||
stat: proc.ki_stat,
|
||||
percent_cpu,
|
||||
mem_resident: proc.ki_rssize.max(0) as u64 * pagesize,
|
||||
mem_virtual: proc.ki_size.max(0) as u64,
|
||||
};
|
||||
Ok(info)
|
||||
name,
|
||||
command: command.filter(|_| long),
|
||||
exe: long
|
||||
.then(|| proc_sysctl(KERN_PROC_PATHNAME, proc.ki_pid).ok())
|
||||
.flatten()
|
||||
.and_then(|path| c_string(&path)),
|
||||
user: user_names.get(proc.ki_uid),
|
||||
user_id: Some(proc.ki_uid),
|
||||
status: Some(process_status(proc.ki_stat)),
|
||||
cpu_usage: percent_cpu,
|
||||
cpu_time: Some(Duration::from_micros(proc.ki_runtime)),
|
||||
mem_size: Some(proc.ki_rssize.max(0) as u64 * pagesize),
|
||||
virtual_size: Some(proc.ki_size as u64),
|
||||
private_size: long
|
||||
.then(|| get_private_resident(proc.ki_pid, pagesize))
|
||||
.flatten(),
|
||||
// `ki_rusage` counts storage I/O in blocks (`ru_inblock`, `ru_oublock`), not
|
||||
// bytes, and the kernel keeps no byte count to report.
|
||||
disk_read: None,
|
||||
disk_written: None,
|
||||
start_time: Some(
|
||||
UNIX_EPOCH
|
||||
+ Duration::from_secs(proc.ki_start.tv_sec.max(0) as u64)
|
||||
+ Duration::from_micros(proc.ki_start.tv_usec.max(0) as u64),
|
||||
),
|
||||
process_group_id: Some(proc.ki_pgid),
|
||||
session_id: Some(proc.ki_sid.into()),
|
||||
priority: Some(proc.ki_pri.pri_level.into()),
|
||||
nice: Some(proc.ki_nice.into()),
|
||||
thread_count: Some(proc.ki_numthreads.into()),
|
||||
cwd: long.then(|| get_cwd(proc.ki_pid)).flatten(),
|
||||
environ: long
|
||||
.then(|| proc_sysctl(KERN_PROC_ENV, proc.ki_pid).ok())
|
||||
.flatten()
|
||||
.map(|env| split_nul(&env)),
|
||||
})
|
||||
})
|
||||
// Remove errors from the list - probably just processes that are gone now
|
||||
.flat_map(|result: io::Result<_>| result.ok())
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -122,21 +130,13 @@ fn check(err: libc::c_int) -> std::io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a bounds-checked way to read a `CStr` from a slice of `c_char`
|
||||
fn read_cstr(slice: &[libc::c_char]) -> &CStr {
|
||||
unsafe {
|
||||
// SAFETY: ensure that c_char and u8 are the same size
|
||||
mem::transmute::<libc::c_char, u8>(0);
|
||||
let slice: &[u8] = mem::transmute(slice);
|
||||
CStr::from_bytes_until_nul(slice).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists every process. `KERN_PROC_PROC` gives one entry per process, whose `ki_runtime` is the
|
||||
/// process's total, while `KERN_PROC_ALL` gives one per thread with that thread's own runtime.
|
||||
fn get_procs() -> io::Result<Vec<libc::kinfo_proc>> {
|
||||
// To understand what's going on here, see the sysctl(3) manpage for FreeBSD.
|
||||
unsafe {
|
||||
const STRUCT_SIZE: usize = mem::size_of::<libc::kinfo_proc>();
|
||||
let ctl_name = [CTL_KERN, KERN_PROC, KERN_PROC_ALL];
|
||||
let ctl_name = [CTL_KERN, KERN_PROC, KERN_PROC_PROC];
|
||||
|
||||
// First, try to figure out how large a buffer we need to allocate
|
||||
// (calling with NULL just tells us that)
|
||||
@@ -172,15 +172,17 @@ fn get_procs() -> io::Result<Vec<libc::kinfo_proc>> {
|
||||
let true_len = data_len.div_ceil(STRUCT_SIZE);
|
||||
vec.set_len(true_len);
|
||||
|
||||
// Sort the procs by pid and then tid before using them
|
||||
vec.sort_by_key(|p| (p.ki_pid, p.ki_tid));
|
||||
// Sort the procs by pid before using them
|
||||
vec.sort_by_key(|p| p.ki_pid);
|
||||
Ok(vec)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_proc_args(pid: i32) -> io::Result<Vec<u8>> {
|
||||
/// Reads the variable-length `kern.proc.<what>.<pid>` sysctl, such as the arguments
|
||||
/// (`KERN_PROC_ARGS`) or environment (`KERN_PROC_ENV`) of a process.
|
||||
fn proc_sysctl(what: i32, pid: i32) -> io::Result<Vec<u8>> {
|
||||
unsafe {
|
||||
let ctl_name = [CTL_KERN, KERN_PROC, KERN_PROC_ARGS, pid];
|
||||
let ctl_name = [CTL_KERN, KERN_PROC, what, pid];
|
||||
|
||||
// First, try to figure out how large a buffer we need to allocate
|
||||
// (calling with NULL just tells us that)
|
||||
@@ -215,6 +217,41 @@ fn get_proc_args(pid: i32) -> io::Result<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The working directory: the path in the `kinfo_file` that `KERN_PROC_CWD` returns.
|
||||
fn get_cwd(pid: i32) -> Option<String> {
|
||||
let data = proc_sysctl(KERN_PROC_CWD, pid).ok()?;
|
||||
c_string(data.get(mem::offset_of!(kinfo_file, kf_path)..)?)
|
||||
}
|
||||
|
||||
/// Resident memory of the process's anonymous mappings (heap, stack and anonymous `mmap`), the
|
||||
/// resident pages of each `KERN_PROC_VMMAP` entry backed by anonymous memory, like Linux's
|
||||
/// `RssAnon`. The entries' `kve_private_resident` (the `PRES` column of `procstat -v`) can't be
|
||||
/// used: it is the page count of the whole VM object, so a mapped file such as a shared library
|
||||
/// would add all of its cached pages, once for each of its mappings.
|
||||
fn get_private_resident(pid: i32, pagesize: u64) -> Option<u64> {
|
||||
let data = proc_sysctl(KERN_PROC_VMMAP, pid).ok()?;
|
||||
let read_int = |offset: usize| {
|
||||
let bytes = data.get(offset..offset + mem::size_of::<i32>())?;
|
||||
Some(i32::from_ne_bytes(bytes.try_into().ok()?))
|
||||
};
|
||||
let mut pages = 0;
|
||||
let mut offset = 0;
|
||||
// The kernel packs the entries, so each one starts with its own size.
|
||||
while offset < data.len() {
|
||||
let size = read_int(offset + mem::offset_of!(kinfo_vmentry, kve_structsize))?;
|
||||
if size <= 0 {
|
||||
break;
|
||||
}
|
||||
let kind = read_int(offset + mem::offset_of!(kinfo_vmentry, kve_type))?;
|
||||
if kind == KVME_TYPE_DEFAULT || kind == KVME_TYPE_SWAP {
|
||||
let resident = read_int(offset + mem::offset_of!(kinfo_vmentry, kve_resident))?;
|
||||
pages += resident.max(0) as u64;
|
||||
}
|
||||
offset += size as usize;
|
||||
}
|
||||
Some(pages * pagesize)
|
||||
}
|
||||
|
||||
/// For getting simple values from the sysctl interface
|
||||
///
|
||||
/// # Safety
|
||||
@@ -256,73 +293,15 @@ fn get_pagesize() -> io::Result<libc::c_int> {
|
||||
unsafe { get_ctl(&[CTL_HW, HW_PAGESIZE]) }
|
||||
}
|
||||
|
||||
impl ProcessInfo {
|
||||
/// PID of process
|
||||
pub fn pid(&self) -> i32 {
|
||||
self.pid
|
||||
}
|
||||
|
||||
/// Parent PID of process
|
||||
pub fn ppid(&self) -> i32 {
|
||||
self.ppid
|
||||
}
|
||||
|
||||
/// Name of command
|
||||
pub fn name(&self) -> String {
|
||||
let argv_name = self
|
||||
.argv
|
||||
.split(|b| *b == 0)
|
||||
.next()
|
||||
.map(String::from_utf8_lossy)
|
||||
.unwrap_or_default()
|
||||
.into_owned();
|
||||
|
||||
if !argv_name.is_empty() {
|
||||
argv_name
|
||||
} else {
|
||||
// Just use the command name alone.
|
||||
self.name.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Full name of command, with arguments
|
||||
pub fn command(&self) -> String {
|
||||
if let Some(last_nul) = self.argv.iter().rposition(|b| *b == 0) {
|
||||
// The command string is NUL separated
|
||||
// Take the string up to the last NUL, then replace the NULs with spaces
|
||||
String::from_utf8_lossy(&self.argv[0..last_nul]).replace("\0", " ")
|
||||
} else {
|
||||
// The argv is empty, so use the name instead
|
||||
self.name()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the status of the process
|
||||
pub fn status(&self) -> String {
|
||||
match self.stat {
|
||||
libc::SIDL | libc::SRUN => "Running",
|
||||
libc::SSLEEP => "Sleeping",
|
||||
libc::SSTOP => "Stopped",
|
||||
libc::SWAIT => "Waiting",
|
||||
libc::SLOCK => "Locked",
|
||||
libc::SZOMB => "Zombie",
|
||||
_ => "Unknown",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// CPU usage as a percent of total
|
||||
pub fn cpu_usage(&self) -> f64 {
|
||||
self.percent_cpu
|
||||
}
|
||||
|
||||
/// Memory size in number of bytes
|
||||
pub fn mem_size(&self) -> u64 {
|
||||
self.mem_resident
|
||||
}
|
||||
|
||||
/// Virtual memory size in bytes
|
||||
pub fn virtual_size(&self) -> u64 {
|
||||
self.mem_virtual
|
||||
/// Names a process state (`ki_stat`).
|
||||
fn process_status(stat: c_char) -> &'static str {
|
||||
match stat {
|
||||
libc::SIDL | libc::SRUN => "Running",
|
||||
libc::SSLEEP => "Sleeping",
|
||||
libc::SSTOP => "Stopped",
|
||||
libc::SWAIT => "Waiting",
|
||||
libc::SLOCK => "Locked",
|
||||
libc::SZOMB => "Zombie",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,16 @@ mod macos;
|
||||
#[cfg(any(target_os = "netbsd", target_os = "openbsd"))]
|
||||
mod netbsd;
|
||||
pub mod os_info;
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
target_os = "linux",
|
||||
target_os = "macos",
|
||||
target_os = "windows",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
mod process;
|
||||
#[cfg(target_family = "unix")]
|
||||
mod unix;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -35,6 +45,17 @@ pub use self::foreground::{
|
||||
|
||||
pub use self::util::*;
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
target_os = "linux",
|
||||
target_os = "macos",
|
||||
target_os = "windows",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
pub use self::process::ProcessInfo;
|
||||
|
||||
#[cfg(target_os = "freebsd")]
|
||||
pub use self::freebsd::*;
|
||||
#[cfg(any(target_os = "android", target_os = "linux"))]
|
||||
|
||||
+220
-220
@@ -1,91 +1,108 @@
|
||||
use crate::process::{ProcessInfo, command_line};
|
||||
use crate::unix::UserNames;
|
||||
use log::info;
|
||||
use nu_utils::time::Instant;
|
||||
use procfs::process::{FDInfo, Io, Process, Stat, Status};
|
||||
use procfs::{ProcError, ProcessCGroups, WithCurrentSystemInfo};
|
||||
use std::path::PathBuf;
|
||||
use procfs::process::{Process, Stat, Status};
|
||||
use procfs::{FromBufRead, ProcResult, WithCurrentSystemInfo};
|
||||
use std::io::Read;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::path::Path;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
pub enum ProcessTask {
|
||||
Process(Process),
|
||||
Task { stat: Box<Stat>, owner: u32 },
|
||||
/// The kernel keeps at most this many bytes of a process name (`TASK_COMM_LEN - 1`).
|
||||
const COMM_MAX_LEN: usize = 15;
|
||||
|
||||
/// One reading of the CPU time a process has used.
|
||||
struct CpuSample {
|
||||
/// Process start time in clock ticks since boot, to detect a reused pid.
|
||||
starttime: u64,
|
||||
/// `utime + stime` from `/proc/<pid>/stat`, in clock ticks (usually 10ms each).
|
||||
ticks: u64,
|
||||
/// The process's CPU-time clock. `None` when the kernel can't read it.
|
||||
clock: Option<Duration>,
|
||||
}
|
||||
|
||||
impl ProcessTask {
|
||||
pub fn stat(&self) -> Result<Stat, ProcError> {
|
||||
match self {
|
||||
ProcessTask::Process(x) => x.stat(),
|
||||
ProcessTask::Task { stat: x, owner: _ } => Ok(*x.clone()),
|
||||
impl CpuSample {
|
||||
fn read(stat: &Stat) -> Self {
|
||||
Self {
|
||||
starttime: stat.starttime,
|
||||
ticks: stat.utime + stat.stime,
|
||||
clock: process_cpu_time(stat.pid),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cmdline(&self) -> Result<Vec<String>, ProcError> {
|
||||
match self {
|
||||
ProcessTask::Process(x) => x.cmdline(),
|
||||
_ => Err(ProcError::Other("not supported".to_string())),
|
||||
/// CPU time used between `prev` and `self`, or `None` if they are different processes.
|
||||
fn since(&self, prev: &CpuSample) -> Option<Duration> {
|
||||
if self.starttime != prev.starttime {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cgroups(&self) -> Result<ProcessCGroups, ProcError> {
|
||||
match self {
|
||||
ProcessTask::Process(x) => x.cgroups(),
|
||||
_ => Err(ProcError::Other("not supported".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fd(&self) -> Result<Vec<FDInfo>, ProcError> {
|
||||
match self {
|
||||
ProcessTask::Process(x) => x.fd()?.collect(),
|
||||
_ => Err(ProcError::Other("not supported".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn loginuid(&self) -> Result<u32, ProcError> {
|
||||
match self {
|
||||
ProcessTask::Process(x) => x.loginuid(),
|
||||
_ => Err(ProcError::Other("not supported".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn owner(&self) -> u32 {
|
||||
match self {
|
||||
ProcessTask::Process(x) => x.uid().unwrap_or(0),
|
||||
ProcessTask::Task { stat: _, owner: x } => *x,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wchan(&self) -> Result<String, ProcError> {
|
||||
match self {
|
||||
ProcessTask::Process(x) => x.wchan(),
|
||||
_ => Err(ProcError::Other("not supported".to_string())),
|
||||
if let (Some(curr), Some(prev)) = (self.clock, prev.clock) {
|
||||
return Some(curr.saturating_sub(prev));
|
||||
}
|
||||
let ticks = self.ticks.saturating_sub(prev.ticks);
|
||||
Some(Duration::from_secs_f64(
|
||||
ticks as f64 / procfs::ticks_per_second() as f64,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProcessInfo {
|
||||
pub pid: i32,
|
||||
pub ppid: i32,
|
||||
pub curr_proc: ProcessTask,
|
||||
pub curr_io: Option<Io>,
|
||||
pub prev_io: Option<Io>,
|
||||
pub curr_stat: Option<Stat>,
|
||||
pub prev_stat: Option<Stat>,
|
||||
pub curr_status: Option<Status>,
|
||||
pub interval: Duration,
|
||||
pub cwd: PathBuf,
|
||||
/// Reads the CPU time a process has used from its CPU-time clock.
|
||||
///
|
||||
/// `/proc/<pid>/stat` counts CPU time in clock ticks, which is too coarse for a 100ms sample: a
|
||||
/// process could only ever show 0%, 10%, 20%... The clock counts nanoseconds and, like `stat`,
|
||||
/// includes the time of threads that have exited. Linux lets any process read the CPU-time
|
||||
/// clock of any other process.
|
||||
fn process_cpu_time(pid: i32) -> Option<Duration> {
|
||||
// The id `clock_getcpuclockid(3)` returns for `pid`: the kernel's
|
||||
// `MAKE_PROCESS_CPUCLOCK(pid, CPUCLOCK_SCHED)`, which glibc, musl and bionic all compute
|
||||
// this way. Calling `clock_getcpuclockid` would cost another syscall to validate the id,
|
||||
// and Android only has it from API level 23.
|
||||
let clock: libc::clockid_t = (!pid << 3) | 2;
|
||||
let mut time = MaybeUninit::<libc::timespec>::uninit();
|
||||
// SAFETY: `time` is valid for `clock_gettime` to write a `timespec` to.
|
||||
if unsafe { libc::clock_gettime(clock, time.as_mut_ptr()) } != 0 {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: `clock_gettime` succeeded, so it filled in `time`.
|
||||
let time = unsafe { time.assume_init() };
|
||||
Some(Duration::new(
|
||||
time.tv_sec.try_into().ok()?,
|
||||
time.tv_nsec.try_into().ok()?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec<ProcessInfo> {
|
||||
/// Reads `/proc/<pid>/status`. The kernel writes the process name into it as raw bytes, which
|
||||
/// aren't valid UTF-8 when the name was cut in the middle of a character, and procfs rejects
|
||||
/// the whole file then, so decode it lossily first.
|
||||
fn read_status(proc: &Process) -> ProcResult<Status> {
|
||||
let mut buf = Vec::new();
|
||||
proc.open_relative("status")?.read_to_end(&mut buf)?;
|
||||
Status::from_buf_read(String::from_utf8_lossy(&buf).as_bytes())
|
||||
}
|
||||
|
||||
/// Reads `/proc/<pid>/environ` as `KEY=value` strings in their original order.
|
||||
fn read_environ(proc: &Process) -> ProcResult<Vec<String>> {
|
||||
let mut buf = Vec::new();
|
||||
proc.open_relative("environ")?.read_to_end(&mut buf)?;
|
||||
Ok(crate::unix::split_nul(&buf))
|
||||
}
|
||||
|
||||
/// Lists every process, measuring CPU usage over `interval`. When `long` is false, the details
|
||||
/// only `ps --long` shows aren't read, so they are `None`. The command line and executable are
|
||||
/// still read when the name needs them.
|
||||
///
|
||||
/// The executable, I/O counts, working directory and environment are only readable for the
|
||||
/// current user's processes (or as root).
|
||||
pub fn collect_proc(interval: Duration, long: bool) -> Vec<ProcessInfo> {
|
||||
let mut base_procs = Vec::new();
|
||||
let mut ret = Vec::new();
|
||||
|
||||
// Take an initial snapshot of process I/O and CPU info, so we can calculate changes over time
|
||||
// Take an initial snapshot of CPU usage, so we can calculate changes over time. Only keep
|
||||
// the pid: holding every `Process` open across the sleep can exhaust file descriptors.
|
||||
if let Ok(all_proc) = procfs::process::all_processes() {
|
||||
for proc in all_proc.flatten() {
|
||||
let io = proc.io().ok();
|
||||
let stat = proc.stat().ok();
|
||||
let time = Instant::now();
|
||||
base_procs.push((proc.pid(), io, stat, time));
|
||||
let sample = proc.stat().ok().map(|stat| CpuSample::read(&stat));
|
||||
base_procs.push((proc.pid(), sample, Instant::now()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,162 +110,145 @@ pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec<ProcessInfo>
|
||||
thread::sleep(interval);
|
||||
|
||||
// now get process info again, build up results
|
||||
for (pid, prev_io, prev_stat, prev_time) in base_procs {
|
||||
let curr_proc_pid = pid;
|
||||
let curr_proc = if let Ok(p) = Process::new(curr_proc_pid) {
|
||||
p
|
||||
} else {
|
||||
info!(
|
||||
"failed to retrieve info for pid={curr_proc_pid}, process probably died between snapshots"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let cwd = curr_proc.cwd().unwrap_or_default();
|
||||
let mut user_names = UserNames::default();
|
||||
base_procs
|
||||
.into_iter()
|
||||
.filter_map(|(pid, prev_sample, prev_time)| {
|
||||
let Some((proc, stat)) = Process::new(pid)
|
||||
.and_then(|proc| proc.stat().map(|stat| (proc, stat)))
|
||||
.ok()
|
||||
else {
|
||||
info!(
|
||||
"failed to retrieve info for pid={pid}, process probably died between snapshots"
|
||||
);
|
||||
return None;
|
||||
};
|
||||
let curr_sample = CpuSample::read(&stat);
|
||||
let interval = Instant::now().saturating_duration_since(prev_time);
|
||||
let cpu_usage = prev_sample
|
||||
.and_then(|prev| curr_sample.since(&prev))
|
||||
.map(|used| used.as_secs_f64() * 100.0 / interval.as_secs_f64());
|
||||
|
||||
let curr_io = curr_proc.io().ok();
|
||||
let curr_stat = curr_proc.stat().ok();
|
||||
let curr_status = curr_proc.status().ok();
|
||||
let curr_time = Instant::now();
|
||||
let interval = curr_time.saturating_duration_since(prev_time);
|
||||
let ppid = curr_proc.stat().map(|p| p.ppid).unwrap_or_default();
|
||||
let curr_proc = ProcessTask::Process(curr_proc);
|
||||
let status = read_status(&proc).ok();
|
||||
// The name restores a name the kernel cut short from the command line or executable.
|
||||
let read_paths = long || stat.comm.len() == COMM_MAX_LEN;
|
||||
let cmdline = read_paths.then(|| proc.cmdline().ok()).flatten();
|
||||
let exe = read_paths.then(|| proc.exe().ok()).flatten();
|
||||
let argv0 = cmdline.as_ref().and_then(|cmd| cmd.first()).map(Path::new);
|
||||
let name = untruncated_name(&stat.comm, [argv0, exe.as_deref()]);
|
||||
let io = long.then(|| proc.io().ok()).flatten();
|
||||
let ticks = stat.utime + stat.stime;
|
||||
|
||||
let proc = ProcessInfo {
|
||||
pid,
|
||||
ppid,
|
||||
curr_proc,
|
||||
curr_io,
|
||||
prev_io,
|
||||
curr_stat,
|
||||
prev_stat,
|
||||
curr_status,
|
||||
interval,
|
||||
cwd,
|
||||
};
|
||||
|
||||
ret.push(proc);
|
||||
}
|
||||
|
||||
ret
|
||||
Some(ProcessInfo {
|
||||
pid,
|
||||
ppid: stat.ppid,
|
||||
name,
|
||||
// Kernel threads like kworker/0:0 have an empty command line.
|
||||
command: cmdline.filter(|_| long).and_then(|cmd| command_line(&cmd)),
|
||||
exe: exe.map(|exe| exe.display().to_string()),
|
||||
user: status
|
||||
.as_ref()
|
||||
.and_then(|status| user_names.get(status.euid)),
|
||||
user_id: status.as_ref().map(|status| status.euid),
|
||||
status: Some(process_status(stat.state)),
|
||||
cpu_usage,
|
||||
cpu_time: Some(Duration::from_secs_f64(
|
||||
ticks as f64 / procfs::ticks_per_second() as f64,
|
||||
)),
|
||||
mem_size: Some(stat.rss_bytes().get()),
|
||||
virtual_size: Some(stat.vsize),
|
||||
// Resident memory that isn't shared with other processes (`RssAnon`)
|
||||
private_size: status
|
||||
.as_ref()
|
||||
.and_then(|status| status.rssanon)
|
||||
.map(|kib| kib.saturating_mul(1024)),
|
||||
disk_read: io.as_ref().map(|io| io.read_bytes),
|
||||
disk_written: io.as_ref().map(|io| io.write_bytes),
|
||||
start_time: stat.starttime().get().ok().map(SystemTime::from),
|
||||
process_group_id: Some(stat.pgrp),
|
||||
session_id: Some(stat.session.into()),
|
||||
// The kernel's scheduling priority
|
||||
priority: Some(stat.priority),
|
||||
nice: Some(stat.nice),
|
||||
thread_count: Some(stat.num_threads),
|
||||
cwd: long
|
||||
.then(|| proc.cwd().ok())
|
||||
.flatten()
|
||||
.map(|cwd| cwd.display().to_string()),
|
||||
environ: long.then(|| read_environ(&proc).ok()).flatten(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl ProcessInfo {
|
||||
/// PID of process
|
||||
pub fn pid(&self) -> i32 {
|
||||
self.pid
|
||||
}
|
||||
/// Returns `comm`, or when the kernel cut it to its 15 byte limit, the file name of the first
|
||||
/// path (argv[0], then the executable) whose name starts with it.
|
||||
fn untruncated_name<'a>(comm: &str, paths: impl IntoIterator<Item = Option<&'a Path>>) -> String {
|
||||
let full_name = (comm.len() == COMM_MAX_LEN)
|
||||
.then(|| {
|
||||
paths
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|path| path.file_name()?.to_str())
|
||||
.find(|name| name.starts_with(comm))
|
||||
})
|
||||
.flatten();
|
||||
full_name.unwrap_or(comm).to_string()
|
||||
}
|
||||
|
||||
/// PPID of process
|
||||
pub fn ppid(&self) -> i32 {
|
||||
self.ppid
|
||||
}
|
||||
|
||||
/// Name of command
|
||||
pub fn name(&self) -> String {
|
||||
if let Some(name) = self.comm() {
|
||||
return name;
|
||||
}
|
||||
// Fall back in case /proc/<pid>/stat source is not available.
|
||||
if let Ok(mut cmd) = self.curr_proc.cmdline()
|
||||
&& let Some(name) = cmd.first_mut()
|
||||
{
|
||||
// Take over the first element and return it without extra allocations
|
||||
// (String::default() is allocation-free).
|
||||
return std::mem::take(name);
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Full name of command, with arguments
|
||||
///
|
||||
/// WARNING: As this does no escaping, this function is lossy. It's OK-ish for display purposes
|
||||
/// but nothing else.
|
||||
// TODO: Maybe rename this to display_command and add escaping compatible with nushell?
|
||||
pub fn command(&self) -> String {
|
||||
if let Ok(cmd) = self.curr_proc.cmdline() {
|
||||
// Things like kworker/0:0 still have the cmdline file in proc, even though it's empty.
|
||||
if !cmd.is_empty() {
|
||||
return cmd.join(" ").replace(['\n', '\t'], " ");
|
||||
}
|
||||
}
|
||||
self.comm().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn cwd(&self) -> String {
|
||||
self.cwd.display().to_string()
|
||||
}
|
||||
|
||||
/// Get the status of the process
|
||||
pub fn status(&self) -> String {
|
||||
if let Ok(p) = self.curr_proc.stat() {
|
||||
match p.state {
|
||||
'S' => "Sleeping",
|
||||
'R' => "Running",
|
||||
'D' => "Disk sleep",
|
||||
'Z' => "Zombie",
|
||||
'T' => "Stopped",
|
||||
't' => "Tracing",
|
||||
'X' => "Dead",
|
||||
'x' => "Dead",
|
||||
'K' => "Wakekill",
|
||||
'W' => "Waking",
|
||||
'P' => "Parked",
|
||||
_ => "Unknown",
|
||||
}
|
||||
} else {
|
||||
"Unknown"
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// CPU usage as a percent of total
|
||||
pub fn cpu_usage(&self) -> f64 {
|
||||
if let Some(cs) = &self.curr_stat {
|
||||
if let Some(ps) = &self.prev_stat {
|
||||
let curr_time = cs.utime + cs.stime;
|
||||
let prev_time = ps.utime + ps.stime;
|
||||
|
||||
let usage_ms =
|
||||
curr_time.saturating_sub(prev_time) * 1000 / procfs::ticks_per_second();
|
||||
let interval_ms =
|
||||
self.interval.as_secs() * 1000 + u64::from(self.interval.subsec_millis());
|
||||
usage_ms as f64 * 100.0 / interval_ms as f64
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Memory size in number of bytes
|
||||
pub fn mem_size(&self) -> u64 {
|
||||
match self.curr_proc.stat() {
|
||||
Ok(p) => p.rss_bytes().get(),
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Working set size in bytes
|
||||
pub fn working_size(&self) -> u64 {
|
||||
self.mem_size()
|
||||
}
|
||||
|
||||
/// Paged memory size in bytes
|
||||
pub fn paged_size(&self) -> u64 {
|
||||
self.curr_status
|
||||
.as_ref()
|
||||
.and_then(|status| status.vmswap)
|
||||
.map(|swap_kib| swap_kib.saturating_mul(1024))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Virtual memory size in bytes
|
||||
pub fn virtual_size(&self) -> u64 {
|
||||
self.curr_proc.stat().map(|p| p.vsize).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn comm(&self) -> Option<String> {
|
||||
self.curr_proc.stat().map(|st| st.comm).ok()
|
||||
/// Names the state letter in `/proc/<pid>/stat`.
|
||||
fn process_status(state: char) -> &'static str {
|
||||
match state {
|
||||
'S' => "Sleeping",
|
||||
'R' => "Running",
|
||||
'D' => "Disk sleep",
|
||||
'Z' => "Zombie",
|
||||
'T' => "Stopped",
|
||||
't' => "Tracing",
|
||||
'X' => "Dead",
|
||||
'x' => "Dead",
|
||||
'K' => "Wakekill",
|
||||
'W' => "Waking",
|
||||
'P' => "Parked",
|
||||
'I' => "Idle",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::untruncated_name;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn untruncated_name_restores_cut_names() {
|
||||
let argv0 = Path::new("/usr/libexec/gnome-shell-calendar-server");
|
||||
assert_eq!(
|
||||
untruncated_name("gnome-shell-cal", [Some(argv0), None]),
|
||||
"gnome-shell-calendar-server"
|
||||
);
|
||||
// argv[0] rewritten by the process: fall back to the executable.
|
||||
let exe = Path::new("/usr/lib/systemd/systemd-journald");
|
||||
assert_eq!(
|
||||
untruncated_name(
|
||||
"systemd-journal",
|
||||
[Some(Path::new("journald: main")), Some(exe)]
|
||||
),
|
||||
"systemd-journald"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn untruncated_name_keeps_names_that_were_not_cut() {
|
||||
// Short names are never cut, even when argv[0] differs.
|
||||
assert_eq!(
|
||||
untruncated_name("bash", [Some(Path::new("-bash")), None]),
|
||||
"bash"
|
||||
);
|
||||
// A 15 byte name that no path extends stays as it is.
|
||||
assert_eq!(
|
||||
untruncated_name("abcdefghijklmno", [Some(Path::new("/bin/other")), None]),
|
||||
"abcdefghijklmno"
|
||||
);
|
||||
}
|
||||
}
|
||||
+302
-320
@@ -1,131 +1,282 @@
|
||||
use crate::process::{ProcessInfo, command_line};
|
||||
use crate::unix::{UserNames, c_string};
|
||||
use libc::{c_int, c_void, size_t};
|
||||
use libproc::libproc::bsd_info::BSDInfo;
|
||||
use libproc::libproc::file_info::{ListFDs, ProcFDType, pidfdinfo};
|
||||
use libproc::libproc::net_info::{InSockInfo, SocketFDInfo, SocketInfoKind, TcpSockInfo};
|
||||
use libproc::libproc::pid_rusage::{RUsageInfoV2, pidrusage};
|
||||
use libproc::libproc::proc_pid::{ListThreads, listpidinfo, pidinfo};
|
||||
use libproc::libproc::task_info::{TaskAllInfo, TaskInfo};
|
||||
use libproc::libproc::proc_pid::{ListThreads, listpidinfo, pidinfo, pidpath};
|
||||
use libproc::libproc::task_info::TaskAllInfo;
|
||||
use libproc::libproc::thread_info::ThreadInfo;
|
||||
use libproc::processes::{ProcFilter, pids_by_type};
|
||||
use mach2::mach_time;
|
||||
use nix::unistd::{Pid, getsid};
|
||||
use nu_utils::time::Instant;
|
||||
use std::cmp;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::mem::{MaybeUninit, offset_of, size_of};
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::ptr;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub struct ProcessInfo {
|
||||
pub pid: i32,
|
||||
pub ppid: i32,
|
||||
pub curr_task: TaskAllInfo,
|
||||
pub prev_task: TaskAllInfo,
|
||||
pub curr_path: Option<PathInfo>,
|
||||
pub curr_threads: Vec<ThreadInfo>,
|
||||
pub curr_udps: Vec<InSockInfo>,
|
||||
pub curr_tcps: Vec<TcpSockInfo>,
|
||||
pub curr_res: Option<RUsageInfoV2>,
|
||||
pub prev_res: Option<RUsageInfoV2>,
|
||||
pub interval: Duration,
|
||||
pub start_time: i64,
|
||||
pub user_id: i64,
|
||||
pub priority: i64,
|
||||
pub task_thread_num: i64,
|
||||
/// One reading of the CPU time a process has used.
|
||||
struct CpuSample {
|
||||
/// Start time in seconds and microseconds, to detect a reused pid.
|
||||
start: (u64, u64),
|
||||
/// User plus system time, in mach ticks.
|
||||
ticks: u64,
|
||||
}
|
||||
|
||||
pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec<ProcessInfo> {
|
||||
impl CpuSample {
|
||||
fn new(task: &TaskAllInfo) -> Self {
|
||||
Self {
|
||||
start: (task.pbsd.pbi_start_tvsec, task.pbsd.pbi_start_tvusec),
|
||||
ticks: task_cpu_ticks(task),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists every process, measuring CPU usage over `interval`. When `long` is false, the details
|
||||
/// only `ps --long` shows aren't read, so they are `None`. The executable's path is still read
|
||||
/// when the name needs it.
|
||||
///
|
||||
/// The kernel only hands out task, thread, argument and working directory details for the
|
||||
/// current user's processes (`/bin/ps` and `top` are setuid root to see everything). The details
|
||||
/// that need that access are `None` for other users' processes, which are still listed with the
|
||||
/// details that are public.
|
||||
pub fn collect_proc(interval: Duration, long: bool) -> Vec<ProcessInfo> {
|
||||
let mut base_procs = Vec::new();
|
||||
let mut ret = Vec::new();
|
||||
let arg_max = get_arg_max();
|
||||
|
||||
if let Ok(procs) = pids_by_type(ProcFilter::All) {
|
||||
for p in procs {
|
||||
if let Ok(task) = pidinfo::<TaskAllInfo>(p as i32, 0) {
|
||||
let res = pidrusage::<RUsageInfoV2>(p as i32).ok();
|
||||
let time = Instant::now();
|
||||
base_procs.push((p as i32, task, res, time));
|
||||
}
|
||||
let sample = pidinfo::<TaskAllInfo>(p as i32, 0)
|
||||
.ok()
|
||||
.map(|task| CpuSample::new(&task));
|
||||
base_procs.push((p as i32, sample, Instant::now()));
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(interval);
|
||||
|
||||
for (pid, prev_task, prev_res, prev_time) in base_procs {
|
||||
let curr_task = if let Ok(task) = pidinfo::<TaskAllInfo>(pid, 0) {
|
||||
task
|
||||
} else {
|
||||
clone_task_all_info(&prev_task)
|
||||
};
|
||||
let ticktime = mach_ticktime();
|
||||
let kinfo = if long { all_kinfo() } else { HashMap::new() };
|
||||
let mut user_names = UserNames::default();
|
||||
base_procs
|
||||
.into_iter()
|
||||
.filter_map(|(pid, prev_sample, prev_time)| {
|
||||
// SAFETY: `proc_bsdshortinfo` is the struct this flavor returns, and it holds only
|
||||
// integers. A non-zero `arg` makes the kernel find zombies too, so this fails only
|
||||
// once the process has been reaped.
|
||||
let bsd_info: libc::proc_bsdshortinfo =
|
||||
unsafe { pid_info(pid, libc::PROC_PIDT_SHORTBSDINFO, 1) }?;
|
||||
let task = pidinfo::<TaskAllInfo>(pid, 0).ok();
|
||||
let interval = Instant::now().saturating_duration_since(prev_time);
|
||||
// A different start time means the pid was reused during the sample.
|
||||
let cpu_usage = prev_sample
|
||||
.zip(task.as_ref().map(CpuSample::new))
|
||||
.filter(|(prev, curr)| prev.start == curr.start)
|
||||
.map(|(prev, curr)| {
|
||||
curr.ticks.saturating_sub(prev.ticks) as f64 * ticktime * 100.0
|
||||
/ interval.as_nanos() as f64
|
||||
});
|
||||
|
||||
let curr_path = get_path_info(pid, arg_max);
|
||||
// Arguments, threads, resource usage and the working directory need the same access
|
||||
// as the task info, so don't ask for them when the kernel already refused.
|
||||
let (args, threads, rusage, cwd) = match &task {
|
||||
Some(task) => (
|
||||
get_path_info(pid, arg_max),
|
||||
get_threads(pid, task),
|
||||
long.then(|| pidrusage::<RUsageInfoV2>(pid).ok()).flatten(),
|
||||
long.then(|| get_cwd(pid)).flatten(),
|
||||
),
|
||||
None => (None, Vec::new(), None, None),
|
||||
};
|
||||
// The name is the file name of the path the process was started with, and the
|
||||
// command its arguments. When those can't be read, both fall back to the executable,
|
||||
// like `/bin/ps` shows, and the name then to the kernel's (16 character) name.
|
||||
let started = args.as_ref().filter(|args| !args.cmd.is_empty());
|
||||
let exe = (long || started.is_none())
|
||||
.then(|| pidpath(pid).ok().map(PathBuf::from))
|
||||
.flatten();
|
||||
let name = started
|
||||
.map(|args| args.exe.as_path())
|
||||
.or(exe.as_deref())
|
||||
.and_then(Path::file_name)
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| {
|
||||
c_string(&bsd_info.pbsi_comm.map(|c| c as u8)).unwrap_or_default()
|
||||
});
|
||||
let command = long
|
||||
.then(|| {
|
||||
started
|
||||
.and_then(|args| command_line(&args.cmd))
|
||||
.or_else(|| exe.as_ref().map(|exe| exe.display().to_string()))
|
||||
})
|
||||
.flatten();
|
||||
let ptinfo = task.as_ref().map(|task| &task.ptinfo);
|
||||
let kinfo = kinfo.get(&pid);
|
||||
|
||||
let threadids = listpidinfo::<ListThreads>(pid, curr_task.ptinfo.pti_threadnum as usize);
|
||||
let mut curr_threads = Vec::new();
|
||||
if let Ok(threadids) = threadids {
|
||||
for t in threadids {
|
||||
if let Ok(thread) = pidinfo::<ThreadInfo>(pid, t) {
|
||||
curr_threads.push(thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(ProcessInfo {
|
||||
pid,
|
||||
ppid: bsd_info.pbsi_ppid as i32,
|
||||
name,
|
||||
command,
|
||||
exe: exe.map(|exe| exe.display().to_string()),
|
||||
user: user_names.get(bsd_info.pbsi_uid),
|
||||
user_id: Some(bsd_info.pbsi_uid),
|
||||
status: process_status(bsd_info.pbsi_status, &threads),
|
||||
cpu_usage,
|
||||
cpu_time: task.as_ref().map(|task| {
|
||||
Duration::from_nanos((task_cpu_ticks(task) as f64 * ticktime) as u64)
|
||||
}),
|
||||
mem_size: ptinfo.map(|info| info.pti_resident_size),
|
||||
virtual_size: ptinfo.map(|info| info.pti_virtual_size),
|
||||
// The physical footprint: the memory the process alone is responsible for,
|
||||
// including what the system compressed or swapped. Activity Monitor shows this
|
||||
// as "Memory".
|
||||
private_size: rusage.as_ref().map(|usage| usage.ri_phys_footprint),
|
||||
disk_read: rusage.as_ref().map(|usage| usage.ri_diskio_bytesread),
|
||||
disk_written: rusage.as_ref().map(|usage| usage.ri_diskio_byteswritten),
|
||||
start_time: kinfo.and_then(KinfoProc::start_time),
|
||||
process_group_id: Some(bsd_info.pbsi_pgid as i32),
|
||||
// `getsid(0)` reads the calling process, so kernel_task's session isn't readable.
|
||||
session_id: (long && pid != 0)
|
||||
.then(|| getsid(Some(Pid::from_raw(pid))).ok())
|
||||
.flatten()
|
||||
.map(|sid| sid.as_raw().into()),
|
||||
// The Mach task priority
|
||||
priority: ptinfo.map(|info| info.pti_priority.into()),
|
||||
nice: kinfo.map(|kinfo| kinfo.p_nice.into()),
|
||||
thread_count: ptinfo.map(|info| info.pti_threadnum.into()),
|
||||
cwd: cwd.map(|cwd| cwd.display().to_string()),
|
||||
// The environment the process started with
|
||||
environ: args.filter(|_| long).map(|args| args.env),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
let mut curr_tcps = Vec::new();
|
||||
let mut curr_udps = Vec::new();
|
||||
/// Reads a fixed-size `proc_pidinfo` flavor, or `None` if the kernel refuses or the process
|
||||
/// doesn't exist. `arg` means something different for each flavor.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `T` must be the struct the kernel returns for `flavor`, and every bit pattern, including all
|
||||
/// zeroes, must be a valid `T`: plain integers and arrays of them, no `bool`, enum or pointer.
|
||||
unsafe fn pid_info<T>(pid: i32, flavor: c_int, arg: u64) -> Option<T> {
|
||||
let size = size_of::<T>() as c_int;
|
||||
let mut info = MaybeUninit::<T>::zeroed();
|
||||
// SAFETY: the buffer is `size` bytes, and the kernel writes at most that many.
|
||||
let written = unsafe { libc::proc_pidinfo(pid, flavor, arg, info.as_mut_ptr().cast(), size) };
|
||||
// SAFETY: the kernel filled the whole buffer, and the caller guarantees that any bytes make
|
||||
// a valid `T`.
|
||||
(written == size).then(|| unsafe { info.assume_init() })
|
||||
}
|
||||
|
||||
let fds = listpidinfo::<ListFDs>(pid, curr_task.pbsd.pbi_nfiles as usize);
|
||||
if let Ok(fds) = fds {
|
||||
for fd in fds {
|
||||
if let ProcFDType::Socket = fd.proc_fdtype.into()
|
||||
&& let Ok(socket) = pidfdinfo::<SocketFDInfo>(pid, fd.proc_fd)
|
||||
{
|
||||
match socket.psi.soi_kind.into() {
|
||||
SocketInfoKind::In => {
|
||||
if socket.psi.soi_protocol == libc::IPPROTO_UDP {
|
||||
let info = unsafe { socket.psi.soi_proto.pri_in };
|
||||
curr_udps.push(info);
|
||||
}
|
||||
}
|
||||
SocketInfoKind::Tcp => {
|
||||
let info = unsafe { socket.psi.soi_proto.pri_tcp };
|
||||
curr_tcps.push(info);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn get_threads(pid: i32, task: &TaskAllInfo) -> Vec<ThreadInfo> {
|
||||
listpidinfo::<ListThreads>(pid, task.ptinfo.pti_threadnum as usize)
|
||||
.map(|ids| {
|
||||
ids.into_iter()
|
||||
.filter_map(|id| pidinfo::<ThreadInfo>(pid, id).ok())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
let curr_res = pidrusage::<RUsageInfoV2>(pid).ok();
|
||||
/// The real current working directory. The `PWD` environment variable only records the
|
||||
/// directory the process was started in.
|
||||
fn get_cwd(pid: i32) -> Option<PathBuf> {
|
||||
// SAFETY: `proc_vnodepathinfo` is the struct this flavor returns, and it holds only integers
|
||||
// and character arrays. The flavor ignores `arg`.
|
||||
let info: libc::proc_vnodepathinfo = unsafe { pid_info(pid, libc::PROC_PIDVNODEPATHINFO, 0) }?;
|
||||
let path: Vec<u8> = info
|
||||
.pvi_cdir
|
||||
.vip_path
|
||||
.as_flattened()
|
||||
.iter()
|
||||
.take_while(|c| **c != 0)
|
||||
.map(|c| *c as u8)
|
||||
.collect();
|
||||
(!path.is_empty()).then(|| PathBuf::from(OsString::from_vec(path)))
|
||||
}
|
||||
|
||||
let curr_time = Instant::now();
|
||||
let interval = curr_time.saturating_duration_since(prev_time);
|
||||
let ppid = curr_task.pbsd.pbi_ppid as i32;
|
||||
let start_time = curr_task.pbsd.pbi_start_tvsec as i64;
|
||||
let user_id = curr_task.pbsd.pbi_uid as i64;
|
||||
let priority = curr_task.ptinfo.pti_priority as i64;
|
||||
let task_thread_num = curr_task.ptinfo.pti_threadnum as i64;
|
||||
/// Darwin's `struct kinfo_proc` from `<sys/sysctl.h>`, which starts with `struct extern_proc`
|
||||
/// from `<sys/proc.h>`. libc doesn't define them for Apple targets, so this names only the
|
||||
/// fields `ps` reads, at their offsets in the 648-byte struct of 64-bit targets, the size of each
|
||||
/// entry `KERN_PROC_ALL` returns.
|
||||
#[repr(C)]
|
||||
struct KinfoProc {
|
||||
/// `kp_proc.p_starttime`
|
||||
p_starttime: libc::timeval,
|
||||
_before_pid: [u8; 40 - size_of::<libc::timeval>()],
|
||||
/// `kp_proc.p_pid`
|
||||
p_pid: libc::pid_t,
|
||||
_before_nice: [u8; 242 - 44],
|
||||
/// `kp_proc.p_nice`
|
||||
p_nice: libc::c_char,
|
||||
_rest: [u8; 648 - 243],
|
||||
}
|
||||
|
||||
let proc = ProcessInfo {
|
||||
pid,
|
||||
ppid,
|
||||
curr_task,
|
||||
prev_task,
|
||||
curr_path,
|
||||
curr_threads,
|
||||
curr_udps,
|
||||
curr_tcps,
|
||||
curr_res,
|
||||
prev_res,
|
||||
interval,
|
||||
start_time,
|
||||
user_id,
|
||||
priority,
|
||||
task_thread_num,
|
||||
};
|
||||
const _: () = assert!(
|
||||
offset_of!(KinfoProc, p_pid) == 40
|
||||
&& offset_of!(KinfoProc, p_nice) == 242
|
||||
&& size_of::<KinfoProc>() == 648
|
||||
);
|
||||
|
||||
ret.push(proc);
|
||||
impl KinfoProc {
|
||||
/// When the process started
|
||||
fn start_time(&self) -> Option<SystemTime> {
|
||||
let start = self.p_starttime;
|
||||
let since_epoch = Duration::from_secs(start.tv_sec.try_into().ok()?)
|
||||
.checked_add(Duration::from_micros(start.tv_usec.try_into().ok()?))?;
|
||||
UNIX_EPOCH.checked_add(since_epoch)
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
/// Reads every process's `kinfo_proc` with one `KERN_PROC_ALL` sysctl, keyed by pid. Unlike
|
||||
/// `proc_pidinfo`, it covers every user's process, kernel_task and zombies included. Reading one
|
||||
/// pid at a time with `KERN_PROC_PID` would walk the kernel's process list once per process.
|
||||
fn all_kinfo() -> HashMap<i32, KinfoProc> {
|
||||
let mut mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_ALL];
|
||||
let mut sysctl = |buffer: *mut c_void, size: &mut size_t| {
|
||||
// SAFETY: `buffer` is either null, which only asks for the size, or has room for `size`
|
||||
// bytes, and the kernel writes at most that many.
|
||||
unsafe {
|
||||
libc::sysctl(
|
||||
mib.as_mut_ptr(),
|
||||
mib.len() as u32,
|
||||
buffer,
|
||||
size,
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
}
|
||||
};
|
||||
// Processes can start between the size query and the read, which then fails with ENOMEM,
|
||||
// so leave some room and try again.
|
||||
for _ in 0..4 {
|
||||
let mut size = 0;
|
||||
if sysctl(ptr::null_mut(), &mut size) != 0 {
|
||||
break;
|
||||
}
|
||||
let mut procs = Vec::<KinfoProc>::with_capacity(size / size_of::<KinfoProc>() + 16);
|
||||
size = procs.capacity() * size_of::<KinfoProc>();
|
||||
if sysctl(procs.as_mut_ptr().cast(), &mut size) != 0 {
|
||||
if io::Error::last_os_error().raw_os_error() == Some(libc::ENOMEM) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// SAFETY: the kernel wrote `size` bytes of whole structs, whose fields are all plain
|
||||
// integers.
|
||||
unsafe { procs.set_len(size / size_of::<KinfoProc>()) };
|
||||
return procs
|
||||
.into_iter()
|
||||
.map(|kinfo| (kinfo.p_pid, kinfo))
|
||||
.collect();
|
||||
}
|
||||
HashMap::new()
|
||||
}
|
||||
|
||||
fn get_arg_max() -> size_t {
|
||||
@@ -146,26 +297,29 @@ fn get_arg_max() -> size_t {
|
||||
arg_max as size_t
|
||||
}
|
||||
|
||||
pub struct PathInfo {
|
||||
pub name: String,
|
||||
pub exe: PathBuf,
|
||||
pub root: PathBuf,
|
||||
pub cmd: Vec<String>,
|
||||
pub env: Vec<String>,
|
||||
pub cwd: PathBuf,
|
||||
/// What `KERN_PROCARGS2` reports: the path passed to `exec`, the arguments and the environment
|
||||
/// the process started with.
|
||||
struct PathInfo {
|
||||
exe: PathBuf,
|
||||
cmd: Vec<String>,
|
||||
env: Vec<String>,
|
||||
}
|
||||
|
||||
unsafe fn get_unchecked_str(cp: *mut u8, start: *mut u8) -> String {
|
||||
unsafe {
|
||||
let len = (cp as usize).saturating_sub(start as usize);
|
||||
let part = std::slice::from_raw_parts(start, len);
|
||||
String::from_utf8_unchecked(part.to_vec())
|
||||
}
|
||||
/// Decodes the bytes from `start` up to `cp`, replacing invalid UTF-8.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `start..cp` must lie inside one initialized buffer.
|
||||
unsafe fn lossy_str(cp: *mut u8, start: *mut u8) -> String {
|
||||
let len = (cp as usize).saturating_sub(start as usize);
|
||||
// SAFETY: the caller guarantees that `start..cp` is initialized memory in one buffer.
|
||||
let part = unsafe { std::slice::from_raw_parts(start, len) };
|
||||
String::from_utf8_lossy(part).into_owned()
|
||||
}
|
||||
|
||||
fn get_path_info(pid: i32, mut size: size_t) -> Option<PathInfo> {
|
||||
let mut proc_args = Vec::with_capacity(size);
|
||||
let ptr: *mut u8 = proc_args.as_mut_slice().as_mut_ptr();
|
||||
let mut proc_args: Vec<u8> = Vec::with_capacity(size);
|
||||
let ptr: *mut u8 = proc_args.as_mut_ptr();
|
||||
|
||||
let mut mib: [c_int; 3] = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as c_int];
|
||||
|
||||
@@ -191,21 +345,7 @@ fn get_path_info(pid: i32, mut size: size_t) -> Option<PathInfo> {
|
||||
while cp < ptr.add(size) && *cp != 0 {
|
||||
cp = cp.offset(1);
|
||||
}
|
||||
let exe = Path::new(get_unchecked_str(cp, start).as_str()).to_path_buf();
|
||||
let name = exe
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
let mut need_root = true;
|
||||
let mut root = Default::default();
|
||||
if exe.is_absolute()
|
||||
&& let Some(parent) = exe.parent()
|
||||
{
|
||||
root = parent.to_path_buf();
|
||||
need_root = false;
|
||||
}
|
||||
let exe = Path::new(lossy_str(cp, start).as_str()).to_path_buf();
|
||||
while cp < ptr.add(size) && *cp == 0 {
|
||||
cp = cp.offset(1);
|
||||
}
|
||||
@@ -215,45 +355,25 @@ fn get_path_info(pid: i32, mut size: size_t) -> Option<PathInfo> {
|
||||
while c < n_args && cp < ptr.add(size) {
|
||||
if *cp == 0 {
|
||||
c += 1;
|
||||
cmd.push(get_unchecked_str(cp, start));
|
||||
cmd.push(lossy_str(cp, start));
|
||||
start = cp.offset(1);
|
||||
}
|
||||
cp = cp.offset(1);
|
||||
}
|
||||
start = cp;
|
||||
let mut env = Vec::new();
|
||||
let mut cwd = PathBuf::default();
|
||||
while cp < ptr.add(size) {
|
||||
if *cp == 0 {
|
||||
if cp == start {
|
||||
break;
|
||||
}
|
||||
let env_str = get_unchecked_str(cp, start);
|
||||
if let Some(pwd) = env_str.strip_prefix("PWD=") {
|
||||
cwd = PathBuf::from(pwd)
|
||||
}
|
||||
env.push(env_str);
|
||||
env.push(lossy_str(cp, start));
|
||||
start = cp.offset(1);
|
||||
}
|
||||
cp = cp.offset(1);
|
||||
}
|
||||
if need_root {
|
||||
for env in env.iter() {
|
||||
if env.starts_with("PATH=") {
|
||||
root = Path::new(&env[6..]).to_path_buf();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(PathInfo {
|
||||
exe,
|
||||
name,
|
||||
root,
|
||||
cmd,
|
||||
env,
|
||||
cwd,
|
||||
})
|
||||
Some(PathInfo { exe, cmd, env })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -263,178 +383,40 @@ fn get_path_info(pid: i32, mut size: size_t) -> Option<PathInfo> {
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_task_all_info(src: &TaskAllInfo) -> TaskAllInfo {
|
||||
let pbsd = BSDInfo {
|
||||
pbi_flags: src.pbsd.pbi_flags,
|
||||
pbi_status: src.pbsd.pbi_status,
|
||||
pbi_xstatus: src.pbsd.pbi_xstatus,
|
||||
pbi_pid: src.pbsd.pbi_pid,
|
||||
pbi_ppid: src.pbsd.pbi_ppid,
|
||||
pbi_uid: src.pbsd.pbi_uid,
|
||||
pbi_gid: src.pbsd.pbi_gid,
|
||||
pbi_ruid: src.pbsd.pbi_ruid,
|
||||
pbi_rgid: src.pbsd.pbi_rgid,
|
||||
pbi_svuid: src.pbsd.pbi_svuid,
|
||||
pbi_svgid: src.pbsd.pbi_svgid,
|
||||
rfu_1: src.pbsd.rfu_1,
|
||||
pbi_comm: src.pbsd.pbi_comm,
|
||||
pbi_name: src.pbsd.pbi_name,
|
||||
pbi_nfiles: src.pbsd.pbi_nfiles,
|
||||
pbi_pgid: src.pbsd.pbi_pgid,
|
||||
pbi_pjobc: src.pbsd.pbi_pjobc,
|
||||
e_tdev: src.pbsd.e_tdev,
|
||||
e_tpgid: src.pbsd.e_tpgid,
|
||||
pbi_nice: src.pbsd.pbi_nice,
|
||||
pbi_start_tvsec: src.pbsd.pbi_start_tvsec,
|
||||
pbi_start_tvusec: src.pbsd.pbi_start_tvusec,
|
||||
};
|
||||
|
||||
// Comments taken from here https://github.com/apple-oss-distributions/xnu/blob/8d741a5de7ff4191bf97d57b9f54c2f6d4a15585/bsd/sys/proc_info.h#L127
|
||||
let ptinfo = TaskInfo {
|
||||
// virtual memory size (bytes)
|
||||
pti_virtual_size: src.ptinfo.pti_virtual_size,
|
||||
// resident memory size (bytes)
|
||||
pti_resident_size: src.ptinfo.pti_resident_size,
|
||||
// total user time
|
||||
pti_total_user: src.ptinfo.pti_total_user,
|
||||
// total system time
|
||||
pti_total_system: src.ptinfo.pti_total_system,
|
||||
// existing threads only user
|
||||
pti_threads_user: src.ptinfo.pti_threads_user,
|
||||
// existing threads only system
|
||||
pti_threads_system: src.ptinfo.pti_threads_system,
|
||||
// default policy for new threads
|
||||
pti_policy: src.ptinfo.pti_policy,
|
||||
// number of page faults
|
||||
pti_faults: src.ptinfo.pti_faults,
|
||||
// number of actual pageins
|
||||
pti_pageins: src.ptinfo.pti_pageins,
|
||||
// number of copy-on-write faults
|
||||
pti_cow_faults: src.ptinfo.pti_cow_faults,
|
||||
// number of messages sent
|
||||
pti_messages_sent: src.ptinfo.pti_messages_sent,
|
||||
// number of messages received
|
||||
pti_messages_received: src.ptinfo.pti_messages_received,
|
||||
// number of mach system calls
|
||||
pti_syscalls_mach: src.ptinfo.pti_syscalls_mach,
|
||||
// number of unix system calls
|
||||
pti_syscalls_unix: src.ptinfo.pti_syscalls_unix,
|
||||
// number of context switches
|
||||
pti_csw: src.ptinfo.pti_csw,
|
||||
// number of threads in the task
|
||||
pti_threadnum: src.ptinfo.pti_threadnum,
|
||||
// number of running threads
|
||||
pti_numrunning: src.ptinfo.pti_numrunning,
|
||||
// task priority
|
||||
pti_priority: src.ptinfo.pti_priority,
|
||||
};
|
||||
TaskAllInfo { pbsd, ptinfo }
|
||||
/// The state of a process. Zombies and stopped processes report theirs in `pbsi_status`, and
|
||||
/// every other process reports `SRUN` there, so its real state comes from its threads.
|
||||
fn process_status(bsd_status: u32, threads: &[ThreadInfo]) -> Option<&'static str> {
|
||||
match bsd_status {
|
||||
libc::SZOMB => return Some("Zombie"),
|
||||
libc::SSTOP => return Some("Stopped"),
|
||||
_ => {}
|
||||
}
|
||||
let state = threads
|
||||
.iter()
|
||||
.map(|t| match t.pth_run_state {
|
||||
1 => 1, // TH_STATE_RUNNING
|
||||
2 => 5, // TH_STATE_STOPPED
|
||||
// The kernel reports a `pth_sleep_time` of 0 for every thread, so a long sleep
|
||||
// can't be told from a short one.
|
||||
3 => 3, // TH_STATE_WAITING
|
||||
4 => 2, // TH_STATE_UNINTERRUPTIBLE
|
||||
5 => 6, // TH_STATE_HALTED
|
||||
_ => 7,
|
||||
})
|
||||
.min()?;
|
||||
Some(match state {
|
||||
1 => "Running",
|
||||
2 => "Uninterruptible",
|
||||
3 => "Sleeping",
|
||||
5 => "Stopped",
|
||||
6 => "Halted",
|
||||
_ => "Unknown",
|
||||
})
|
||||
}
|
||||
|
||||
impl ProcessInfo {
|
||||
/// PID of process
|
||||
pub fn pid(&self) -> i32 {
|
||||
self.pid
|
||||
}
|
||||
|
||||
/// Parent PID of process
|
||||
pub fn ppid(&self) -> i32 {
|
||||
self.ppid
|
||||
}
|
||||
|
||||
/// Name of command
|
||||
pub fn name(&self) -> String {
|
||||
if let Some(path) = &self.curr_path {
|
||||
if !path.cmd.is_empty() {
|
||||
let command_path = &path.exe;
|
||||
|
||||
if let Some(command_name) = command_path.file_name() {
|
||||
command_name.to_string_lossy().to_string()
|
||||
} else {
|
||||
command_path.to_string_lossy().to_string()
|
||||
}
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
|
||||
/// Full name of command, with arguments
|
||||
pub fn command(&self) -> String {
|
||||
if let Some(path) = &self.curr_path {
|
||||
if !path.cmd.is_empty() {
|
||||
path.cmd.join(" ").replace(['\n', '\t'], " ")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the status of the process
|
||||
pub fn status(&self) -> String {
|
||||
let mut state = 7;
|
||||
for t in &self.curr_threads {
|
||||
let s = match t.pth_run_state {
|
||||
1 => 1, // TH_STATE_RUNNING
|
||||
2 => 5, // TH_STATE_STOPPED
|
||||
3 => {
|
||||
if t.pth_sleep_time > 20 {
|
||||
4
|
||||
} else {
|
||||
3
|
||||
}
|
||||
} // TH_STATE_WAITING
|
||||
4 => 2, // TH_STATE_UNINTERRUPTIBLE
|
||||
5 => 6, // TH_STATE_HALTED
|
||||
_ => 7,
|
||||
};
|
||||
state = cmp::min(s, state);
|
||||
}
|
||||
let state = match state {
|
||||
0 => "",
|
||||
1 => "Running",
|
||||
2 => "Uninterruptible",
|
||||
3 => "Sleep",
|
||||
4 => "Waiting",
|
||||
5 => "Stopped",
|
||||
6 => "Halted",
|
||||
_ => "?",
|
||||
};
|
||||
state.to_string()
|
||||
}
|
||||
|
||||
/// CPU usage as a percent of total
|
||||
pub fn cpu_usage(&self) -> f64 {
|
||||
let curr_time =
|
||||
self.curr_task.ptinfo.pti_total_user + self.curr_task.ptinfo.pti_total_system;
|
||||
let prev_time =
|
||||
self.prev_task.ptinfo.pti_total_user + self.prev_task.ptinfo.pti_total_system;
|
||||
let usage_ticks = curr_time.saturating_sub(prev_time);
|
||||
let interval_us = self.interval.as_micros();
|
||||
let ticktime_us = mach_ticktime() / 1000.0;
|
||||
usage_ticks as f64 * 100.0 * ticktime_us / interval_us as f64
|
||||
}
|
||||
|
||||
/// Memory size in number of bytes
|
||||
pub fn mem_size(&self) -> u64 {
|
||||
self.curr_task.ptinfo.pti_resident_size
|
||||
}
|
||||
|
||||
/// Virtual memory size in bytes
|
||||
pub fn virtual_size(&self) -> u64 {
|
||||
self.curr_task.ptinfo.pti_virtual_size
|
||||
}
|
||||
|
||||
pub fn cwd(&self) -> String {
|
||||
self.curr_path
|
||||
.as_ref()
|
||||
.map(|cur_path| cur_path.cwd.display().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
/// User plus system time of a task, in mach ticks.
|
||||
fn task_cpu_ticks(task: &TaskAllInfo) -> u64 {
|
||||
task.ptinfo.pti_total_user + task.ptinfo.pti_total_system
|
||||
}
|
||||
|
||||
/// The Macos kernel returns process times in mach ticks rather than nanoseconds. To get times in
|
||||
|
||||
+144
-104
@@ -1,14 +1,18 @@
|
||||
//! This is used for both NetBSD and OpenBSD, because they are fairly similar.
|
||||
|
||||
use itertools::{EitherOrBoth, Itertools};
|
||||
use libc::{CTL_HW, CTL_KERN, KERN_PROC_ALL, KERN_PROC_ARGS, KERN_PROC_ARGV, sysctl};
|
||||
use libc::{
|
||||
CTL_HW, CTL_KERN, KERN_PROC_ALL, KERN_PROC_ARGS, KERN_PROC_ARGV, KERN_PROC_ENV, sysctl,
|
||||
};
|
||||
use std::{
|
||||
io,
|
||||
mem::{self, MaybeUninit},
|
||||
ptr,
|
||||
time::Duration,
|
||||
time::{Duration, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use crate::process::{ProcessInfo, name_and_command};
|
||||
use crate::unix::{UserNames, c_string, split_nul};
|
||||
use nu_utils::time::Instant;
|
||||
|
||||
#[cfg(target_os = "netbsd")]
|
||||
@@ -16,25 +20,27 @@ type KInfoProc = libc::kinfo_proc2;
|
||||
#[cfg(target_os = "openbsd")]
|
||||
type KInfoProc = libc::kinfo_proc;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProcessInfo {
|
||||
pub pid: i32,
|
||||
pub ppid: i32,
|
||||
pub argv: Vec<u8>,
|
||||
pub stat: i8,
|
||||
pub percent_cpu: f64,
|
||||
pub mem_resident: u64, // in bytes
|
||||
pub mem_virtual: u64, // in bytes
|
||||
}
|
||||
/// The kernel stores `p_nice` offset by `NZERO` (from `<sys/param.h>`), so that it fits in an
|
||||
/// unsigned byte. libc only defines `NZERO` for FreeBSD, where it is 0.
|
||||
const NZERO: i64 = 20;
|
||||
|
||||
pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec<ProcessInfo> {
|
||||
compare_procs(interval).unwrap_or_else(|err| {
|
||||
/// The `kern.proc_args.<pid>.cwd` sysctl from NetBSD's `<sys/sysctl.h>`, which libc doesn't
|
||||
/// define.
|
||||
#[cfg(target_os = "netbsd")]
|
||||
const KERN_PROC_CWD: i32 = 6;
|
||||
|
||||
/// Lists every process, measuring CPU usage over `interval`. When `long` is false, the details
|
||||
/// only `ps --long` shows aren't read, so they are `None`.
|
||||
///
|
||||
/// The environment is only readable for the current user's processes (or as root).
|
||||
pub fn collect_proc(interval: Duration, long: bool) -> Vec<ProcessInfo> {
|
||||
compare_procs(interval, long).unwrap_or_else(|err| {
|
||||
log::warn!("Failed to get processes: {}", err);
|
||||
vec![]
|
||||
})
|
||||
}
|
||||
|
||||
fn compare_procs(interval: Duration) -> io::Result<Vec<ProcessInfo>> {
|
||||
fn compare_procs(interval: Duration, long: bool) -> io::Result<Vec<ProcessInfo>> {
|
||||
let pagesize = get_pagesize()? as u64;
|
||||
|
||||
// Compare two full snapshots of all of the processes over the interval
|
||||
@@ -45,45 +51,106 @@ fn compare_procs(interval: Duration) -> io::Result<Vec<ProcessInfo>> {
|
||||
let true_interval = Instant::now().saturating_duration_since(now);
|
||||
let true_interval_sec = true_interval.as_secs_f64();
|
||||
|
||||
let mut user_names = UserNames::default();
|
||||
|
||||
// Join the processes between the two snapshots
|
||||
Ok(procs_a
|
||||
.into_iter()
|
||||
.merge_join_by(procs_b.into_iter(), |a, b| a.p_pid.cmp(&b.p_pid))
|
||||
.map(|proc| {
|
||||
// Take both snapshotted processes if we can, but if not then just keep the one that
|
||||
// exists and set prev_proc to None
|
||||
let (prev_proc, proc) = match proc {
|
||||
.merge_join_by(procs_b, |a, b| a.p_pid.cmp(&b.p_pid))
|
||||
.filter_map(|procs| {
|
||||
let (prev_proc, proc) = match procs {
|
||||
EitherOrBoth::Both(a, b) => (Some(a), b),
|
||||
EitherOrBoth::Left(a) => (None, a),
|
||||
// Started during the sample, so there's nothing to compare against.
|
||||
EitherOrBoth::Right(b) => (None, b),
|
||||
// Exited during the sample.
|
||||
EitherOrBoth::Left(_) => return None,
|
||||
};
|
||||
|
||||
// The kernel only reports the run and start times (`p_u*`) of a process that isn't
|
||||
// a zombie (`p_uvalid`), and OpenBSD leaves the start time out for one that is exiting.
|
||||
let rtime =
|
||||
|proc: &KInfoProc| proc.p_rtime_sec as f64 + proc.p_rtime_usec as f64 / 1_000_000.0;
|
||||
let start_time = (proc.p_uvalid != 0 && proc.p_ustart_sec != 0).then(|| {
|
||||
UNIX_EPOCH
|
||||
+ Duration::from_secs(proc.p_ustart_sec as u64)
|
||||
+ Duration::from_micros(proc.p_ustart_usec as u64)
|
||||
});
|
||||
|
||||
// The percentage CPU is the ratio of how much runtime occurred for the process out of
|
||||
// the true measured interval that occurred.
|
||||
let percent_cpu = if let Some(prev_proc) = prev_proc {
|
||||
let prev_rtime =
|
||||
prev_proc.p_rtime_sec as f64 + prev_proc.p_rtime_usec as f64 / 1_000_000.0;
|
||||
let rtime = proc.p_rtime_sec as f64 + proc.p_rtime_usec as f64 / 1_000_000.0;
|
||||
100. * (rtime - prev_rtime).max(0.) / true_interval_sec
|
||||
} else {
|
||||
0.0
|
||||
// the true measured interval that occurred. A different start time means the pid was
|
||||
// reused.
|
||||
let same_process = |prev: &KInfoProc| {
|
||||
start_time.is_none()
|
||||
|| (prev.p_ustart_sec, prev.p_ustart_usec)
|
||||
== (proc.p_ustart_sec, proc.p_ustart_usec)
|
||||
};
|
||||
let percent_cpu = prev_proc.filter(same_process).map(|prev_proc| {
|
||||
100. * (rtime(&proc) - rtime(&prev_proc)).max(0.) / true_interval_sec
|
||||
});
|
||||
|
||||
Ok(ProcessInfo {
|
||||
#[allow(clippy::unnecessary_cast, reason = "`c_char` is `u8` on some targets")]
|
||||
let comm = proc.p_comm.map(|c| c as u8);
|
||||
// Keep the process even when its arguments can't be read (a zombie, say).
|
||||
let (name, command) = name_and_command(
|
||||
&get_proc_args(proc.p_pid, KERN_PROC_ARGV).unwrap_or_default(),
|
||||
c_string(&comm).unwrap_or_default(),
|
||||
);
|
||||
|
||||
Some(ProcessInfo {
|
||||
pid: proc.p_pid,
|
||||
ppid: proc.p_ppid,
|
||||
argv: get_proc_args(proc.p_pid, KERN_PROC_ARGV)?,
|
||||
stat: proc.p_stat,
|
||||
percent_cpu,
|
||||
mem_resident: proc.p_vm_rssize.max(0) as u64 * pagesize,
|
||||
name,
|
||||
command: command.filter(|_| long),
|
||||
#[cfg(target_os = "netbsd")]
|
||||
mem_virtual: proc.p_vm_msize.max(0) as u64 * pagesize,
|
||||
exe: long
|
||||
.then(|| get_proc_args(proc.p_pid, libc::KERN_PROC_PATHNAME).ok())
|
||||
.flatten()
|
||||
.and_then(|path| c_string(&path)),
|
||||
// OpenBSD doesn't expose the path of a process's executable.
|
||||
#[cfg(target_os = "openbsd")]
|
||||
mem_virtual: proc.p_vm_map_size.max(0) as u64 * pagesize,
|
||||
exe: None,
|
||||
user: user_names.get(proc.p_uid),
|
||||
user_id: Some(proc.p_uid),
|
||||
status: Some(process_status(proc.p_stat)),
|
||||
cpu_usage: percent_cpu,
|
||||
cpu_time: (proc.p_uvalid != 0).then(|| Duration::from_secs_f64(rtime(&proc))),
|
||||
mem_size: Some(proc.p_vm_rssize.max(0) as u64 * pagesize),
|
||||
#[cfg(target_os = "netbsd")]
|
||||
virtual_size: Some(proc.p_vm_msize.max(0) as u64 * pagesize),
|
||||
// OpenBSD never fills in `p_vm_map_size`, so add up the text, data and stack
|
||||
// segments, as its `ps` and `top` do.
|
||||
#[cfg(target_os = "openbsd")]
|
||||
virtual_size: Some(
|
||||
(proc.p_vm_tsize.max(0) as u64
|
||||
+ proc.p_vm_dsize.max(0) as u64
|
||||
+ proc.p_vm_ssize.max(0) as u64)
|
||||
* pagesize,
|
||||
),
|
||||
// The kernel only reports a process's total resident size (`p_vm_rssize`) and
|
||||
// the virtual sizes of its segments, nothing about what is private.
|
||||
private_size: None,
|
||||
// `kinfo_proc` counts storage I/O in blocks (`p_uru_inblock`, `p_uru_oublock`),
|
||||
// not bytes, and the kernel keeps no byte count to report.
|
||||
disk_read: None,
|
||||
disk_written: None,
|
||||
start_time,
|
||||
process_group_id: Some(proc.p__pgid),
|
||||
session_id: Some(proc.p_sid.into()),
|
||||
priority: Some(proc.p_priority as i64),
|
||||
nice: Some(i64::from(proc.p_nice) - NZERO),
|
||||
#[cfg(target_os = "netbsd")]
|
||||
thread_count: Some(proc.p_nlwps as i64),
|
||||
// OpenBSD's `kinfo_proc` has no thread count; counting threads means listing
|
||||
// them with `KERN_PROC_SHOW_THREADS`.
|
||||
#[cfg(target_os = "openbsd")]
|
||||
thread_count: None,
|
||||
cwd: long.then(|| get_cwd(proc.p_pid)).flatten(),
|
||||
environ: long
|
||||
.then(|| get_proc_args(proc.p_pid, KERN_PROC_ENV).ok())
|
||||
.flatten()
|
||||
.map(|env| split_nul(&env)),
|
||||
})
|
||||
})
|
||||
// Remove errors from the list - probably just processes that are gone now
|
||||
.flat_map(|result: io::Result<_>| result.ok())
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -260,6 +327,28 @@ fn get_proc_args(pid: i32, what: i32) -> io::Result<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The working directory, from the `kern.proc_args.<pid>.cwd` (NetBSD) or `kern.proc_cwd.<pid>`
|
||||
/// (OpenBSD) sysctl.
|
||||
fn get_cwd(pid: i32) -> Option<String> {
|
||||
#[cfg(target_os = "netbsd")]
|
||||
let ctl_name = [CTL_KERN, KERN_PROC_ARGS, pid, KERN_PROC_CWD];
|
||||
#[cfg(target_os = "openbsd")]
|
||||
let ctl_name = [CTL_KERN, libc::KERN_PROC_CWD, pid];
|
||||
let mut buf = vec![0u8; libc::PATH_MAX as usize];
|
||||
let mut len = buf.len();
|
||||
// SAFETY: `buf` is writable for `len` bytes.
|
||||
check(unsafe {
|
||||
sysctl_get(
|
||||
ctl_name.as_ptr(),
|
||||
ctl_name.len() as u32,
|
||||
buf.as_mut_ptr().cast(),
|
||||
&mut len,
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
c_string(buf.get(..len)?)
|
||||
}
|
||||
|
||||
/// For getting simple values from the sysctl interface
|
||||
///
|
||||
/// # Safety
|
||||
@@ -299,71 +388,22 @@ fn get_pagesize() -> io::Result<libc::c_int> {
|
||||
unsafe { get_ctl(&[CTL_HW, HW_PAGESIZE]) }
|
||||
}
|
||||
|
||||
impl ProcessInfo {
|
||||
/// PID of process
|
||||
pub fn pid(&self) -> i32 {
|
||||
self.pid
|
||||
}
|
||||
|
||||
/// Parent PID of process
|
||||
pub fn ppid(&self) -> i32 {
|
||||
self.ppid
|
||||
}
|
||||
|
||||
/// Name of command
|
||||
pub fn name(&self) -> String {
|
||||
self.argv
|
||||
.split(|b| *b == 0)
|
||||
.next()
|
||||
.map(String::from_utf8_lossy)
|
||||
.unwrap_or_default()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Full name of command, with arguments
|
||||
pub fn command(&self) -> String {
|
||||
if let Some(last_nul) = self.argv.iter().rposition(|b| *b == 0) {
|
||||
// The command string is NUL separated
|
||||
// Take the string up to the last NUL, then replace the NULs with spaces
|
||||
String::from_utf8_lossy(&self.argv[0..last_nul]).replace("\0", " ")
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the status of the process
|
||||
pub fn status(&self) -> String {
|
||||
// see sys/proc.h (OpenBSD), sys/lwp.h (NetBSD)
|
||||
// the names given here are the NetBSD ones, starting with LS*, but the OpenBSD ones are
|
||||
// the same, just starting with S* instead
|
||||
match self.stat {
|
||||
1 /* LSIDL */ => "",
|
||||
2 /* LSRUN */ => "Waiting",
|
||||
3 /* LSSLEEP */ => "Sleeping",
|
||||
4 /* LSSTOP */ => "Stopped",
|
||||
5 /* LSZOMB */ => "Zombie",
|
||||
#[cfg(target_os = "openbsd")] // removed in NetBSD
|
||||
6 /* LSDEAD */ => "Dead",
|
||||
7 /* LSONPROC */ => "Running",
|
||||
#[cfg(target_os = "netbsd")] // doesn't exist in OpenBSD
|
||||
8 /* LSSUSPENDED */ => "Suspended",
|
||||
_ => "Unknown",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// CPU usage as a percent of total
|
||||
pub fn cpu_usage(&self) -> f64 {
|
||||
self.percent_cpu
|
||||
}
|
||||
|
||||
/// Memory size in number of bytes
|
||||
pub fn mem_size(&self) -> u64 {
|
||||
self.mem_resident
|
||||
}
|
||||
|
||||
/// Virtual memory size in bytes
|
||||
pub fn virtual_size(&self) -> u64 {
|
||||
self.mem_virtual
|
||||
/// Names a process state (`p_stat`), from `<sys/lwp.h>` on NetBSD and `<sys/proc.h>` on
|
||||
/// OpenBSD. The names given here are the NetBSD ones, starting with LS*; the OpenBSD ones are the
|
||||
/// same, just starting with S* instead.
|
||||
fn process_status(stat: i8) -> &'static str {
|
||||
match stat {
|
||||
1 /* LSIDL */ => "",
|
||||
2 /* LSRUN */ => "Waiting",
|
||||
3 /* LSSLEEP */ => "Sleeping",
|
||||
4 /* LSSTOP */ => "Stopped",
|
||||
5 /* LSZOMB */ => "Zombie",
|
||||
// OpenBSD's zombies are SDEAD (its SZOMB is unused), which its `ps` shows as `Z`.
|
||||
#[cfg(target_os = "openbsd")] // removed in NetBSD
|
||||
6 /* LSDEAD */ => "Zombie",
|
||||
7 /* LSONPROC */ => "Running",
|
||||
#[cfg(target_os = "netbsd")] // doesn't exist in OpenBSD
|
||||
8 /* LSSUSPENDED */ => "Suspended",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
//! The process listing that every platform's `collect_proc` returns.
|
||||
|
||||
use itertools::Itertools;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
/// The id of the user a process runs as: the numeric uid on unix.
|
||||
#[cfg(unix)]
|
||||
pub(crate) type UserId = u32;
|
||||
/// The id of the user a process runs as: the SID string, such as `S-1-5-18`, on Windows.
|
||||
#[cfg(windows)]
|
||||
pub(crate) type UserId = String;
|
||||
|
||||
/// A process, with the same details on every platform.
|
||||
///
|
||||
/// A detail is `None` when the operating system doesn't make it available for the process (other
|
||||
/// users' processes, for example), and the details only `ps --long` shows are `None` when
|
||||
/// `collect_proc` was asked for the short listing. Each platform's `collect_proc` documents where
|
||||
/// the values come from.
|
||||
pub struct ProcessInfo {
|
||||
pub(crate) pid: i32,
|
||||
pub(crate) ppid: i32,
|
||||
pub(crate) name: String,
|
||||
/// `None` when the command line can't be read, so `command()` shows the name
|
||||
pub(crate) command: Option<String>,
|
||||
pub(crate) exe: Option<String>,
|
||||
pub(crate) user: Option<String>,
|
||||
pub(crate) user_id: Option<UserId>,
|
||||
pub(crate) status: Option<&'static str>,
|
||||
/// Percent of one CPU core over the sample, `None` for a process that started during it
|
||||
pub(crate) cpu_usage: Option<f64>,
|
||||
pub(crate) cpu_time: Option<Duration>,
|
||||
pub(crate) mem_size: Option<u64>,
|
||||
pub(crate) virtual_size: Option<u64>,
|
||||
pub(crate) private_size: Option<u64>,
|
||||
pub(crate) disk_read: Option<u64>,
|
||||
pub(crate) disk_written: Option<u64>,
|
||||
pub(crate) start_time: Option<SystemTime>,
|
||||
pub(crate) process_group_id: Option<i32>,
|
||||
pub(crate) session_id: Option<i64>,
|
||||
pub(crate) priority: Option<i64>,
|
||||
pub(crate) nice: Option<i64>,
|
||||
pub(crate) thread_count: Option<i64>,
|
||||
pub(crate) cwd: Option<String>,
|
||||
pub(crate) environ: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl ProcessInfo {
|
||||
/// Process id
|
||||
pub fn pid(&self) -> i32 {
|
||||
self.pid
|
||||
}
|
||||
|
||||
/// Parent process id
|
||||
pub fn ppid(&self) -> i32 {
|
||||
self.ppid
|
||||
}
|
||||
|
||||
/// Name of the program
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Command line with arguments, or the name when the command line can't be read
|
||||
pub fn command(&self) -> &str {
|
||||
self.command.as_deref().unwrap_or(&self.name)
|
||||
}
|
||||
|
||||
/// Path of the executable
|
||||
pub fn exe(&self) -> Option<&str> {
|
||||
self.exe.as_deref()
|
||||
}
|
||||
|
||||
/// Name of the user the process runs as
|
||||
pub fn user(&self) -> Option<&str> {
|
||||
self.user.as_deref()
|
||||
}
|
||||
|
||||
/// Effective user id
|
||||
#[cfg(unix)]
|
||||
pub fn user_id(&self) -> Option<u32> {
|
||||
self.user_id
|
||||
}
|
||||
|
||||
/// The SID of the user, such as `S-1-5-18`
|
||||
#[cfg(windows)]
|
||||
pub fn user_id(&self) -> Option<&str> {
|
||||
self.user_id.as_deref()
|
||||
}
|
||||
|
||||
/// State of the process, such as "Running" or "Sleeping"
|
||||
pub fn status(&self) -> Option<&'static str> {
|
||||
self.status
|
||||
}
|
||||
|
||||
/// CPU usage as a percent of one core over the sampling interval
|
||||
pub fn cpu_usage(&self) -> Option<f64> {
|
||||
self.cpu_usage
|
||||
}
|
||||
|
||||
/// Total CPU time (user + system) used since the process started
|
||||
pub fn cpu_time(&self) -> Option<Duration> {
|
||||
self.cpu_time
|
||||
}
|
||||
|
||||
/// Resident memory in bytes
|
||||
pub fn mem_size(&self) -> Option<u64> {
|
||||
self.mem_size
|
||||
}
|
||||
|
||||
/// Size of the virtual address space in bytes
|
||||
pub fn virtual_size(&self) -> Option<u64> {
|
||||
self.virtual_size
|
||||
}
|
||||
|
||||
/// Memory only this process uses, in bytes
|
||||
pub fn private_size(&self) -> Option<u64> {
|
||||
self.private_size
|
||||
}
|
||||
|
||||
/// Bytes read since the process started
|
||||
pub fn disk_read(&self) -> Option<u64> {
|
||||
self.disk_read
|
||||
}
|
||||
|
||||
/// Bytes written since the process started
|
||||
pub fn disk_written(&self) -> Option<u64> {
|
||||
self.disk_written
|
||||
}
|
||||
|
||||
/// When the process started
|
||||
pub fn start_time(&self) -> Option<SystemTime> {
|
||||
self.start_time
|
||||
}
|
||||
|
||||
/// Process group id
|
||||
pub fn process_group_id(&self) -> Option<i32> {
|
||||
self.process_group_id
|
||||
}
|
||||
|
||||
/// Session id
|
||||
pub fn session_id(&self) -> Option<i64> {
|
||||
self.session_id
|
||||
}
|
||||
|
||||
/// Scheduling priority
|
||||
pub fn priority(&self) -> Option<i64> {
|
||||
self.priority
|
||||
}
|
||||
|
||||
/// Nice value
|
||||
pub fn nice(&self) -> Option<i64> {
|
||||
self.nice
|
||||
}
|
||||
|
||||
/// Number of threads
|
||||
pub fn thread_count(&self) -> Option<i64> {
|
||||
self.thread_count
|
||||
}
|
||||
|
||||
/// Current working directory
|
||||
pub fn cwd(&self) -> Option<&str> {
|
||||
self.cwd.as_deref()
|
||||
}
|
||||
|
||||
/// Environment variables as `KEY=value` strings
|
||||
pub fn environ(&self) -> Option<Vec<String>> {
|
||||
self.environ.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Joins a process's arguments into the command line `ps` shows, with newlines and tabs turned
|
||||
/// into spaces so that each process stays on one line. `None` when there are no arguments.
|
||||
pub(crate) fn command_line<S: AsRef<str>>(args: &[S]) -> Option<String> {
|
||||
(!args.is_empty()).then(|| {
|
||||
args.iter()
|
||||
.map(AsRef::as_ref)
|
||||
.join(" ")
|
||||
.replace(['\n', '\t'], " ")
|
||||
})
|
||||
}
|
||||
|
||||
/// Names a process the way the BSDs' `ps` does: by its first argument, or when the arguments
|
||||
/// can't be read (a zombie's or a system process's), by the kernel's name for it, `comm`. The
|
||||
/// command is the arguments, which `ProcessInfo::command` replaces with the name when there are
|
||||
/// none. `argv` is the arguments as the kernel reports them, each one ending with a NUL.
|
||||
#[cfg(any(
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd",
|
||||
test
|
||||
))]
|
||||
pub(crate) fn name_and_command(argv: &[u8], comm: String) -> (String, Option<String>) {
|
||||
let args: Vec<_> = match argv.iter().rposition(|b| *b == 0) {
|
||||
Some(end) => argv[..end]
|
||||
.split(|b| *b == 0)
|
||||
.map(String::from_utf8_lossy)
|
||||
.collect(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
let name = match args.first() {
|
||||
Some(argv0) if !argv0.is_empty() => argv0.to_string(),
|
||||
_ => comm,
|
||||
};
|
||||
(name, command_line(&args))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{command_line, name_and_command};
|
||||
|
||||
#[test]
|
||||
fn command_line_keeps_each_process_on_one_line() {
|
||||
assert_eq!(command_line::<&str>(&[]), None);
|
||||
assert_eq!(
|
||||
command_line(&["printf", "a\nb", "c\td"]).as_deref(),
|
||||
Some("printf a b c d")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bsd_names_come_from_argv0_then_comm() {
|
||||
let (name, command) = name_and_command(b"/bin/sh\0-c\0\0echo hi\0", "sh".into());
|
||||
assert_eq!(name, "/bin/sh");
|
||||
// An empty argument still takes its place.
|
||||
assert_eq!(command.as_deref(), Some("/bin/sh -c echo hi"));
|
||||
|
||||
// A zombie's or system process's arguments can't be read.
|
||||
let (name, command) = name_and_command(b"", "zombie".into());
|
||||
assert_eq!((name.as_str(), command), ("zombie", None));
|
||||
|
||||
// A process that cleared its argv[0]
|
||||
let (name, _) = name_and_command(b"\0arg\0", "kept".into());
|
||||
assert_eq!(name, "kept");
|
||||
}
|
||||
}
|
||||
@@ -18,3 +18,52 @@ pub fn get_umask() -> u32 {
|
||||
|
||||
uucore::mode::get_umask()
|
||||
}
|
||||
|
||||
/// Resolves user ids to account names for a process listing, looking each id up only once.
|
||||
///
|
||||
/// Many processes share a handful of users, and each lookup can go through the system's user
|
||||
/// database (e.g. Open Directory on macOS), so caching keeps `ps` fast.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct UserNames(std::collections::HashMap<u32, Option<String>>);
|
||||
|
||||
impl UserNames {
|
||||
/// Returns the account name for `uid`, or `None` if no account has that id.
|
||||
pub(crate) fn get(&mut self, uid: u32) -> Option<String> {
|
||||
self.0
|
||||
.entry(uid)
|
||||
.or_insert_with(|| {
|
||||
nu_utils::filesystem::users::get_user_by_uid(uid.into()).map(|user| user.name)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits NUL-separated strings, such as a process's environment, skipping empty entries.
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
target_os = "linux",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
pub(crate) fn split_nul(bytes: &[u8]) -> Vec<String> {
|
||||
bytes
|
||||
.split(|b| *b == 0)
|
||||
.filter(|entry| !entry.is_empty())
|
||||
.map(|entry| String::from_utf8_lossy(entry).into_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Reads a NUL-terminated string from the start of `bytes`, or `None` if it is empty.
|
||||
#[cfg(any(
|
||||
target_os = "macos",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
pub(crate) fn c_string(bytes: &[u8]) -> Option<String> {
|
||||
let string = std::ffi::CStr::from_bytes_until_nul(bytes)
|
||||
.ok()?
|
||||
.to_string_lossy();
|
||||
(!string.is_empty()).then(|| string.into_owned())
|
||||
}
|
||||
+414
-633
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user