diff --git a/Cargo.lock b/Cargo.lock index 208a82b0a2..64c014013f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 9e45fbbb4a..4b5a3061c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index feb54b45f2..0c9442b605 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index eb22d8d38e..69d783590e 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -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 diff --git a/crates/nu-command/src/platform/kill.rs b/crates/nu-command/src/platform/kill.rs index 2d897328ff..ee6d262eb3 100644 --- a/crates/nu-command/src/platform/kill.rs +++ b/crates/nu-command/src/platform/kill.rs @@ -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 { diff --git a/crates/nu-command/src/system/ps.rs b/crates/nu-command/src/system/ps.rs index c9c2da8e44..6922bd360a 100644 --- a/crates/nu-command/src/system/ps.rs +++ b/crates/nu-command/src/system/ps.rs @@ -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 { - let mut output = vec![]; let span = call.head; let long = call.has_flag(engine_state, stack, "long")?; + let filesize = |bytes: Option| { + 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 = 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::::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, diff --git a/crates/nu-command/tests/commands/job.rs b/crates/nu-command/tests/commands/job.rs index 2242a1d287..06cef78fcb 100644 --- a/crates/nu-command/tests/commands/job.rs +++ b/crates/nu-command/tests/commands/job.rs @@ -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"; } diff --git a/crates/nu-config/default_files/doc_config.nu b/crates/nu-config/default_files/doc_config.nu index a5fb4b885f..b0df4ace43 100644 --- a/crates/nu-config/default_files/doc_config.nu +++ b/crates/nu-config/default_files/doc_config.nu @@ -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" # } # ----- diff --git a/crates/nu-system/Cargo.toml b/crates/nu-system/Cargo.toml index b93173438f..5d29a490bd 100644 --- a/crates/nu-system/Cargo.toml +++ b/crates/nu-system/Cargo.toml @@ -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", diff --git a/crates/nu-system/examples/sys_demo.rs b/crates/nu-system/examples/sys_demo.rs index 088c314757..46a66fd9e3 100644 --- a/crates/nu-system/examples/sys_demo.rs +++ b/crates/nu-system/examples/sys_demo.rs @@ -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, ) } diff --git a/crates/nu-system/src/freebsd.rs b/crates/nu-system/src/freebsd.rs index c8cf271b16..969808e9e2 100644 --- a/crates/nu-system/src/freebsd.rs +++ b/crates/nu-system/src/freebsd.rs @@ -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, - 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 { - 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 { + compare_procs(interval, long).unwrap_or_else(|err| { log::warn!("Failed to get processes: {}", err); vec![] }) } -fn compare_procs(interval: Duration) -> io::Result> { +fn compare_procs(interval: Duration, long: bool) -> io::Result> { 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> { 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::(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> { // To understand what's going on here, see the sysctl(3) manpage for FreeBSD. unsafe { const STRUCT_SIZE: usize = mem::size_of::(); - 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> { 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> { +/// Reads the variable-length `kern.proc..` 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> { 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> { } } +/// The working directory: the path in the `kinfo_file` that `KERN_PROC_CWD` returns. +fn get_cwd(pid: i32) -> Option { + 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 { + let data = proc_sysctl(KERN_PROC_VMMAP, pid).ok()?; + let read_int = |offset: usize| { + let bytes = data.get(offset..offset + mem::size_of::())?; + 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 { 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", } } diff --git a/crates/nu-system/src/lib.rs b/crates/nu-system/src/lib.rs index da3c67b56c..c5fc065f5d 100644 --- a/crates/nu-system/src/lib.rs +++ b/crates/nu-system/src/lib.rs @@ -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"))] diff --git a/crates/nu-system/src/linux.rs b/crates/nu-system/src/linux.rs index 4025f49501..1c52e557b6 100644 --- a/crates/nu-system/src/linux.rs +++ b/crates/nu-system/src/linux.rs @@ -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, 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//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, } -impl ProcessTask { - pub fn stat(&self) -> Result { - 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, 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 { + if self.starttime != prev.starttime { + return None; } - } - - pub fn cgroups(&self) -> Result { - match self { - ProcessTask::Process(x) => x.cgroups(), - _ => Err(ProcError::Other("not supported".to_string())), - } - } - - pub fn fd(&self) -> Result, ProcError> { - match self { - ProcessTask::Process(x) => x.fd()?.collect(), - _ => Err(ProcError::Other("not supported".to_string())), - } - } - - pub fn loginuid(&self) -> Result { - 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 { - 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, - pub prev_io: Option, - pub curr_stat: Option, - pub prev_stat: Option, - pub curr_status: Option, - pub interval: Duration, - pub cwd: PathBuf, +/// Reads the CPU time a process has used from its CPU-time clock. +/// +/// `/proc//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 { + // 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::::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 { +/// Reads `/proc//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 { + 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//environ` as `KEY=value` strings in their original order. +fn read_environ(proc: &Process) -> ProcResult> { + 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 { 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 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>) -> 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//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 { - self.curr_proc.stat().map(|st| st.comm).ok() +/// Names the state letter in `/proc//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" + ); } } diff --git a/crates/nu-system/src/macos.rs b/crates/nu-system/src/macos.rs index 2e66dc1060..b1eb8b3373 100644 --- a/crates/nu-system/src/macos.rs +++ b/crates/nu-system/src/macos.rs @@ -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, - pub curr_threads: Vec, - pub curr_udps: Vec, - pub curr_tcps: Vec, - pub curr_res: Option, - pub prev_res: Option, - 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 { +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 { 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::(p as i32, 0) { - let res = pidrusage::(p as i32).ok(); - let time = Instant::now(); - base_procs.push((p as i32, task, res, time)); - } + let sample = pidinfo::(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::(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::(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::(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::(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::(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(pid: i32, flavor: c_int, arg: u64) -> Option { + let size = size_of::() as c_int; + let mut info = MaybeUninit::::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::(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::(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 { + listpidinfo::(pid, task.ptinfo.pti_threadnum as usize) + .map(|ids| { + ids.into_iter() + .filter_map(|id| pidinfo::(pid, id).ok()) + .collect() + }) + .unwrap_or_default() +} - let curr_res = pidrusage::(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 { + // 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 = 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 ``, which starts with `struct extern_proc` +/// from ``. 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::()], + /// `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::() == 648 +); - ret.push(proc); +impl KinfoProc { + /// When the process started + fn start_time(&self) -> Option { + 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 { + 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::::with_capacity(size / size_of::() + 16); + size = procs.capacity() * size_of::(); + 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::()) }; + 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, - pub env: Vec, - 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, + env: Vec, } -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 { - 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 = 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 { 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 { 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 { } } -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 diff --git a/crates/nu-system/src/netbsd.rs b/crates/nu-system/src/netbsd.rs index ada92c5bfb..9baf1ed234 100644 --- a/crates/nu-system/src/netbsd.rs +++ b/crates/nu-system/src/netbsd.rs @@ -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, - 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 ``), 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 { - compare_procs(interval).unwrap_or_else(|err| { +/// The `kern.proc_args..cwd` sysctl from NetBSD's ``, 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 { + compare_procs(interval, long).unwrap_or_else(|err| { log::warn!("Failed to get processes: {}", err); vec![] }) } -fn compare_procs(interval: Duration) -> io::Result> { +fn compare_procs(interval: Duration, long: bool) -> io::Result> { 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> { 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> { } } +/// The working directory, from the `kern.proc_args..cwd` (NetBSD) or `kern.proc_cwd.` +/// (OpenBSD) sysctl. +fn get_cwd(pid: i32) -> Option { + #[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 { 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 `` on NetBSD and `` 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", } } diff --git a/crates/nu-system/src/process.rs b/crates/nu-system/src/process.rs new file mode 100644 index 0000000000..e9cc14baa8 --- /dev/null +++ b/crates/nu-system/src/process.rs @@ -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, + pub(crate) exe: Option, + pub(crate) user: Option, + pub(crate) user_id: Option, + 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, + pub(crate) cpu_time: Option, + pub(crate) mem_size: Option, + pub(crate) virtual_size: Option, + pub(crate) private_size: Option, + pub(crate) disk_read: Option, + pub(crate) disk_written: Option, + pub(crate) start_time: Option, + pub(crate) process_group_id: Option, + pub(crate) session_id: Option, + pub(crate) priority: Option, + pub(crate) nice: Option, + pub(crate) thread_count: Option, + pub(crate) cwd: Option, + pub(crate) environ: Option>, +} + +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 { + 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 { + self.cpu_usage + } + + /// Total CPU time (user + system) used since the process started + pub fn cpu_time(&self) -> Option { + self.cpu_time + } + + /// Resident memory in bytes + pub fn mem_size(&self) -> Option { + self.mem_size + } + + /// Size of the virtual address space in bytes + pub fn virtual_size(&self) -> Option { + self.virtual_size + } + + /// Memory only this process uses, in bytes + pub fn private_size(&self) -> Option { + self.private_size + } + + /// Bytes read since the process started + pub fn disk_read(&self) -> Option { + self.disk_read + } + + /// Bytes written since the process started + pub fn disk_written(&self) -> Option { + self.disk_written + } + + /// When the process started + pub fn start_time(&self) -> Option { + self.start_time + } + + /// Process group id + pub fn process_group_id(&self) -> Option { + self.process_group_id + } + + /// Session id + pub fn session_id(&self) -> Option { + self.session_id + } + + /// Scheduling priority + pub fn priority(&self) -> Option { + self.priority + } + + /// Nice value + pub fn nice(&self) -> Option { + self.nice + } + + /// Number of threads + pub fn thread_count(&self) -> Option { + 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> { + 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>(args: &[S]) -> Option { + (!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) { + 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"); + } +} diff --git a/crates/nu-system/src/unix.rs b/crates/nu-system/src/unix.rs index 9ea0c55fcc..7f49df9834 100644 --- a/crates/nu-system/src/unix.rs +++ b/crates/nu-system/src/unix.rs @@ -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>); + +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 { + 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 { + 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 { + let string = std::ffi::CStr::from_bytes_until_nul(bytes) + .ok()? + .to_string_lossy(); + (!string.is_empty()).then(|| string.into_owned()) +} diff --git a/crates/nu-system/src/windows.rs b/crates/nu-system/src/windows.rs index 2d5fb788f5..73e7137a07 100644 --- a/crates/nu-system/src/windows.rs +++ b/crates/nu-system/src/windows.rs @@ -1,27 +1,29 @@ // Attribution: a lot of this came from procs https://github.com/dalance/procs // and sysinfo https://github.com/GuillaumeGomez/sysinfo -use chrono::offset::TimeZone; -use chrono::{Local, NaiveDate}; +use crate::process::{ProcessInfo, command_line}; use libc::c_void; +use ntapi::ntexapi::{ + NtQuerySystemInformation, SYSTEM_PROCESS_INFORMATION, SYSTEM_THREAD_INFORMATION, + SystemProcessInformation, +}; +use ntapi::ntkeapi; use ntapi::ntrtl::RTL_USER_PROCESS_PARAMETERS; use ntapi::ntwow64::{PEB32, RTL_USER_PROCESS_PARAMETERS32}; use nu_utils::time::Instant; -use std::cell::RefCell; use std::collections::HashMap; use std::ffi::OsString; use std::mem::{MaybeUninit, size_of, zeroed}; use std::os::windows::ffi::OsStringExt; -use std::path::PathBuf; use std::ptr; use std::ptr::null_mut; use std::sync::LazyLock; use std::thread; -use std::time::Duration; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use windows::core::{PCWSTR, PWSTR}; +use windows::core::{Owned, PCWSTR, PWSTR}; use windows::Wdk::System::SystemServices::RtlGetVersion; use windows::Wdk::System::Threading::{ @@ -30,403 +32,318 @@ use windows::Wdk::System::Threading::{ }; use windows::Win32::Foundation::{ - CloseHandle, FALSE, FILETIME, HANDLE, HLOCAL, HMODULE, LocalFree, MAX_PATH, - STATUS_BUFFER_OVERFLOW, STATUS_BUFFER_TOO_SMALL, STATUS_INFO_LENGTH_MISMATCH, UNICODE_STRING, + FALSE, HANDLE, HLOCAL, STATUS_BUFFER_OVERFLOW, STATUS_BUFFER_TOO_SMALL, + STATUS_INFO_LENGTH_MISMATCH, UNICODE_STRING, }; +use windows::Win32::Security::Authorization::ConvertSidToStringSidW; use windows::Win32::Security::{ AdjustTokenPrivileges, GetTokenInformation, LookupAccountSidW, LookupPrivilegeValueW, PSID, - SE_DEBUG_NAME, SE_PRIVILEGE_ENABLED, SID, SID_NAME_USE, TOKEN_ADJUST_PRIVILEGES, TOKEN_GROUPS, - TOKEN_PRIVILEGES, TOKEN_QUERY, TOKEN_USER, TokenGroups, TokenUser, + SE_DEBUG_NAME, SE_PRIVILEGE_ENABLED, SID_NAME_USE, TOKEN_ADJUST_PRIVILEGES, TOKEN_PRIVILEGES, + TOKEN_QUERY, TOKEN_USER, TokenUser, }; use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; -use windows::Win32::System::Diagnostics::ToolHelp::{ - CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next, TH32CS_SNAPPROCESS, -}; use windows::Win32::System::Memory::{MEMORY_BASIC_INFORMATION, VirtualQueryEx}; -use windows::Win32::System::ProcessStatus::{ - GetModuleBaseNameW, GetProcessMemoryInfo, K32EnumProcesses, PROCESS_MEMORY_COUNTERS, - PROCESS_MEMORY_COUNTERS_EX, -}; - use windows::Win32::System::SystemInformation::OSVERSIONINFOEXW; use windows::Win32::System::Threading::{ - GetCurrentProcess, GetPriorityClass, GetProcessIoCounters, GetProcessTimes, IO_COUNTERS, - OpenProcess, OpenProcessToken, PEB, PROCESS_BASIC_INFORMATION, PROCESS_QUERY_INFORMATION, - PROCESS_VM_READ, + GetCurrentProcess, OpenProcess, OpenProcessToken, PEB, PROCESS_ACCESS_RIGHTS, + PROCESS_BASIC_INFORMATION, PROCESS_NAME_WIN32, PROCESS_QUERY_INFORMATION, + PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ, QueryFullProcessImageNameW, }; use windows::Win32::UI::Shell::CommandLineToArgvW; -pub struct ProcessInfo { - pub pid: i32, - pub command: String, - pub ppid: i32, - pub start_time: chrono::DateTime, - pub cpu_info: CpuInfo, - pub memory_info: MemoryInfo, - pub disk_info: DiskInfo, - pub user: SidName, - pub groups: Vec, - pub priority: u32, - pub thread: i32, - pub interval: Duration, - pub cmd: Vec, - pub environ: Vec, - pub cwd: PathBuf, -} - -#[derive(Default)] -pub struct MemoryInfo { - pub page_fault_count: u64, - pub peak_working_set_size: u64, - pub working_set_size: u64, - pub quota_peak_paged_pool_usage: u64, - pub quota_paged_pool_usage: u64, - pub quota_peak_non_paged_pool_usage: u64, - pub quota_non_paged_pool_usage: u64, - pub page_file_usage: u64, - pub peak_page_file_usage: u64, - pub private_usage: u64, -} - -#[derive(Default)] -pub struct DiskInfo { - pub prev_read: u64, - pub prev_write: u64, - pub curr_read: u64, - pub curr_write: u64, -} - -#[derive(Default)] -pub struct CpuInfo { - pub prev_sys: u64, - pub prev_user: u64, - pub curr_sys: u64, - pub curr_user: u64, -} - -pub fn collect_proc(interval: Duration, _with_thread: bool) -> Vec { - let mut base_procs = Vec::new(); - let mut ret = Vec::new(); - +/// 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`. +/// +/// Everything but the user, executable path, command line, environment, working directory and +/// console process group comes from a system-wide snapshot that covers every process, including +/// protected ones. Those six need a handle to the process, so they are missing for processes we +/// aren't allowed to open. +pub fn collect_proc(interval: Duration, long: bool) -> Vec { let _ = set_privilege(); - for pid in get_pids() { - let handle = get_handle(pid); - - if let Some(handle) = handle { - let times = get_times(handle); - let io = get_io(handle); - - let time = Instant::now(); - - if let (Some((_, _, sys, user)), Some((read, write))) = (times, io) { - base_procs.push((pid, sys, user, read, write, time)); - } - } - } - + let prev = process_snapshot(); + let prev_time = Instant::now(); thread::sleep(interval); + let curr = process_snapshot(); + let interval = Instant::now().saturating_duration_since(prev_time); - let (mut ppids, mut threads) = get_ppid_threads(); + let prev: HashMap, Option)> = prev + .into_iter() + .map(|p| (p.pid, (p.start_time, p.cpu_time))) + .collect(); - for (pid, prev_sys, prev_user, prev_read, prev_write, prev_time) in base_procs { - let ppid = ppids.remove(&pid); - let thread = threads.remove(&pid); - let handle = get_handle(pid); - - if let Some(handle) = handle { - let command = get_command(handle); - let memory_info = get_memory_info(handle); - let times = get_times(handle); - let io = get_io(handle); - - let start_time = if let Some((start, _, _, _)) = times { - // 11_644_473_600 is the number of seconds between the Windows epoch (1601-01-01) and - // the Linux epoch (1970-01-01). - let Some(time) = chrono::Duration::try_seconds(start as i64 / 10_000_000) else { - continue; - }; - let base = - NaiveDate::from_ymd_opt(1601, 1, 1).and_then(|nd| nd.and_hms_opt(0, 0, 0)); - if let Some(base) = base { - let time = base + time; - Local.from_utc_datetime(&time) - } else { - continue; - } - } else { - let time = - NaiveDate::from_ymd_opt(1601, 1, 1).and_then(|nt| nt.and_hms_opt(0, 0, 0)); - if let Some(time) = time { - Local.from_utc_datetime(&time) - } else { - continue; - } - }; - - let cpu_info = if let Some((_, _, curr_sys, curr_user)) = times { - Some(CpuInfo { - prev_sys, - prev_user, - curr_sys, - curr_user, - }) - } else { - None - }; - - let disk_info = if let Some((curr_read, curr_write)) = io { - Some(DiskInfo { - prev_read, - prev_write, - curr_read, - curr_write, - }) - } else { - None - }; - - let user = get_user(handle); - let groups = get_groups(handle); - - let priority = get_priority(handle); - - let curr_time = Instant::now(); - let interval = curr_time.saturating_duration_since(prev_time); - - let mut all_ok = true; - all_ok &= command.is_some(); - all_ok &= cpu_info.is_some(); - all_ok &= memory_info.is_some(); - all_ok &= disk_info.is_some(); - all_ok &= user.is_some(); - all_ok &= groups.is_some(); - all_ok &= thread.is_some(); - - if all_ok { - let (proc_cmd, proc_env, proc_cwd) = match unsafe { get_process_params(handle) } { - Ok(pp) => (pp.0, pp.1, pp.2), - Err(_) => (vec![], vec![], PathBuf::new()), - }; - let command = command.unwrap_or_default(); - let ppid = ppid.unwrap_or(0); - let cpu_info = cpu_info.unwrap_or_default(); - let memory_info = memory_info.unwrap_or_default(); - let disk_info = disk_info.unwrap_or_default(); - let user = user.unwrap_or_else(|| SidName { - sid: vec![], - name: None, - domainname: None, - }); - let groups = groups.unwrap_or_default(); - let thread = thread.unwrap_or_default(); - - let proc = ProcessInfo { - pid, - command, - ppid, - start_time, - cpu_info, - memory_info, - disk_info, - user, - groups, - priority, - thread, - interval, - cmd: proc_cmd, - environ: proc_env, - cwd: proc_cwd, - }; - - ret.push(proc); - } - - unsafe { - let _ = CloseHandle(handle); - } - } - } - - ret + let mut user_names = HashMap::new(); + curr.into_iter() + // The System Idle Process is the CPUs' idle time rather than a process. + .filter(|p| p.pid != 0) + .map(|mut p| { + p.cpu_usage = prev + .get(&p.pid) + // A different creation time means the pid was reused. + .filter(|(start_time, _)| *start_time == p.start_time) + .and_then(|&(_, prev_cpu)| Some(p.cpu_time?.saturating_sub(prev_cpu?))) + .map(|used| used.as_secs_f64() * 100.0 / interval.as_secs_f64()); + read_details(&mut p, long, &mut user_names); + p + }) + .collect() } +/// Lists every process with `NtQuerySystemInformation(SystemProcessInformation)`. Unlike +/// opening each process, this works for protected and other users' processes too. +fn process_snapshot() -> Vec { + // `u64` elements keep the entries 8-byte aligned. + let mut buffer: Vec = Vec::new(); + let mut len = 0u32; + // Processes can start between the size query and the read, so retry a few times. + for _ in 0..8 { + let capacity = buffer.len() * size_of::(); + // SAFETY: the buffer is `capacity` bytes long. + let status = unsafe { + NtQuerySystemInformation( + SystemProcessInformation, + buffer.as_mut_ptr().cast(), + capacity as u32, + &mut len, + ) + }; + if status == STATUS_INFO_LENGTH_MISMATCH.0 { + let wanted = len as usize + 64 * 1024; + buffer.resize(wanted.div_ceil(size_of::()), 0); + continue; + } + if status < 0 { + return Vec::new(); + } + // The entries end with `NextEntryOffset == 0`, so the buffer size is enough of a bound. + return parse_snapshot(&buffer, capacity); + } + Vec::new() +} + +/// Walks the `SYSTEM_PROCESS_INFORMATION` entries the kernel wrote to the first `len` bytes of +/// `buffer`. Thread records or an image name that would reach past `len` are left out. The details +/// that need a handle to the process, and the CPU usage, which needs two snapshots, are `None`. +fn parse_snapshot(buffer: &[u64], len: usize) -> Vec { + // Every pointer into the buffer is derived from `base`, which may read all of it. `Threads` + // is declared with one element, so a pointer taken from an entry's `Threads` field may not + // read the other `NumberOfThreads - 1` records the kernel wrote after it. + let base = buffer.as_ptr().cast::(); + let mut entries = Vec::new(); + let mut offset = 0; + while offset + size_of::() <= len { + // SAFETY: the entry is inside the buffer (checked above) and 8-byte aligned, because the + // buffer is and the kernel rounds every entry up to a multiple of 8 bytes. + let info = unsafe { &*base.add(offset).cast::() }; + let threads_start = offset + std::mem::offset_of!(SYSTEM_PROCESS_INFORMATION, Threads); + let thread_count = info.NumberOfThreads as usize; + let threads = if thread_count + .checked_mul(size_of::()) + .is_some_and(|size| size <= len - threads_start) + { + // SAFETY: the records are inside the buffer (checked above) and aligned like the + // entry. + unsafe { + std::slice::from_raw_parts( + base.add(threads_start).cast::(), + thread_count, + ) + } + } else { + &[] + }; + // SAFETY: these `LARGE_INTEGER` unions are plain 64-bit integers. + let (user_time, kernel_time, create_time, private_working_set, read_bytes, write_bytes) = unsafe { + ( + *info.UserTime.QuadPart(), + *info.KernelTime.QuadPart(), + *info.CreateTime.QuadPart(), + *info.WorkingSetPrivateSize.QuadPart(), + *info.ReadTransferCount.QuadPart(), + *info.WriteTransferCount.QuadPart(), + ) + }; + // The kernel copies the name into the buffer after the thread records and points + // `ImageName.Buffer` at the copy. It's null for the System Idle Process. + let name_bytes = usize::from(info.ImageName.Length); + let name = (info.ImageName.Buffer as usize) + .checked_sub(base as usize) + .filter(|&start| { + start % 2 == 0 + && len + .checked_sub(start) + .is_some_and(|room| name_bytes <= room) + }) + .map(|start| { + // SAFETY: the name's `Length` bytes are inside the buffer (checked above) and + // aligned for UTF-16. + String::from_utf16_lossy(unsafe { + std::slice::from_raw_parts(base.add(start).cast::(), name_bytes / 2) + }) + }) + .unwrap_or_default(); + let base_priority = i64::from(info.BasePriority); + entries.push(ProcessInfo { + pid: info.UniqueProcessId as usize as i32, + ppid: info.InheritedFromUniqueProcessId as usize as i32, + name, + command: None, + exe: None, + user: None, + user_id: None, + status: process_status(threads), + cpu_usage: None, + // `UserTime` and `KernelTime` count 100ns units. + cpu_time: Some(Duration::from_nanos( + ((user_time + kernel_time) as u64).saturating_mul(100), + )), + mem_size: Some(info.WorkingSetSize as u64), + virtual_size: Some(info.VirtualSize as u64), + // The private working set: resident memory that isn't shared with other processes. + // Task Manager shows this as "Memory". + private_size: Some(private_working_set as u64), + // These count all of the process's I/O: files, devices, pipes and network. + disk_read: Some(read_bytes as u64), + disk_written: Some(write_bytes as u64), + start_time: filetime_to_system_time(create_time), + process_group_id: None, + // The Terminal Services session + session_id: Some(info.SessionId.into()), + // The base priority, e.g. 8 for normal priority + priority: Some(base_priority), + // The priority class on the unix nice scale, mapped the way libuv (and Node.js's + // `os.getPriority()`) does + nice: Some(nice_from_base_priority(base_priority)), + thread_count: Some(info.NumberOfThreads.into()), + cwd: None, + environ: None, + }); + if info.NextEntryOffset == 0 { + break; + } + offset += info.NextEntryOffset as usize; + } + entries +} + +/// Derives a process state from its threads, with the same names as on unix: "Running" if any +/// thread is running or ready to run, "Suspended" if every thread is suspended, else "Sleeping". +fn process_status(threads: &[SYSTEM_THREAD_INFORMATION]) -> Option<&'static str> { + if threads.is_empty() { + return None; + } + let running = threads.iter().any(|t| { + matches!( + t.ThreadState, + ntkeapi::Running | ntkeapi::Ready | ntkeapi::Standby | ntkeapi::DeferredReady + ) + }); + let suspended = threads + .iter() + .all(|t| t.ThreadState == ntkeapi::Waiting && t.WaitReason == ntkeapi::Suspended); + Some(if running { + "Running" + } else if suspended { + "Suspended" + } else { + "Sleeping" + }) +} + +/// Converts a `FILETIME` (100ns units since 1601-01-01 UTC) to a `SystemTime`. +fn filetime_to_system_time(filetime: i64) -> Option { + // 100ns intervals between 1601-01-01 and the unix epoch, 1970-01-01. + const UNIX_EPOCH_AS_FILETIME: i64 = 116_444_736_000_000_000; + let since_epoch = u64::try_from(filetime.checked_sub(UNIX_EPOCH_AS_FILETIME)?).ok()?; + Some(UNIX_EPOCH + Duration::from_nanos(since_epoch.checked_mul(100)?)) +} + +/// Opens a handle to the process with `access`, which closes when it drops. +fn open_process(pid: i32, access: PROCESS_ACCESS_RIGHTS) -> Option> { + // SAFETY: `OpenProcess` takes only plain values, and fails for a pid we can't open. + let handle = unsafe { OpenProcess(access, false, pid as u32) }.ok()?; + // SAFETY: the handle is new, so nothing else closes it. + Some(unsafe { Owned::new(handle) }) +} + +/// Fills in what needs a handle to the process: the user and, when `long` is true, the +/// executable path, command line, environment, working directory and console process group. +fn read_details( + info: &mut ProcessInfo, + long: bool, + user_names: &mut HashMap>, +) { + // The environment, working directory and process group are in the process's memory, which + // needs VM_READ. The limited right is granted for more processes (elevated ones, for + // example) and is enough for the user, the executable and, on Windows 8.1 and newer, the + // command line. + let full_access = long + .then(|| open_process(info.pid, PROCESS_QUERY_INFORMATION | PROCESS_VM_READ)) + .flatten(); + let has_vm_read = full_access.is_some(); + let Some(handle) = + full_access.or_else(|| open_process(info.pid, PROCESS_QUERY_LIMITED_INFORMATION)) + else { + return; + }; + if let Some((sid, name)) = get_user(*handle, user_names) { + info.user_id = Some(sid); + info.user = name; + } + if !long { + return; + } + info.exe = get_exe(*handle); + // SAFETY: `handle` has VM_READ access whenever `has_vm_read` is set, and it stays open until + // it drops at the end of this function. + if !(has_vm_read && unsafe { read_process_params(*handle, info) }.is_ok()) { + info.command = command_line(&query_cmd_line(*handle)); + } +} + +/// Full path of the process's executable. Needs only `PROCESS_QUERY_LIMITED_INFORMATION`. +fn get_exe(handle: HANDLE) -> Option { + // Long-path aware executables can live under paths longer than `MAX_PATH`. + let mut buffer = vec![0u16; 32 * 1024]; + let mut len = buffer.len() as u32; + // SAFETY: `buffer` holds `len` UTF-16 units. On success `len` is the number written, not + // counting the terminating NUL. + unsafe { + QueryFullProcessImageNameW( + handle, + PROCESS_NAME_WIN32, + PWSTR::from_raw(buffer.as_mut_ptr()), + &mut len, + ) + } + .ok()?; + Some(String::from_utf16_lossy(buffer.get(..len as usize)?)) +} + +/// Enables `SeDebugPrivilege` for nushell, when its user has it (administrators do), so that it +/// can open other users' processes. fn set_privilege() -> bool { unsafe { - let handle = GetCurrentProcess(); - let mut token: HANDLE = zeroed(); - let ret = OpenProcessToken(handle, TOKEN_ADJUST_PRIVILEGES, &mut token); - if ret.is_err() { + let mut token = HANDLE::default(); + if OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &mut token).is_err() { return false; } + // The token closes when it drops. + let token = Owned::new(token); let mut tps: TOKEN_PRIVILEGES = zeroed(); tps.PrivilegeCount = 1; - if LookupPrivilegeValueW(PCWSTR::null(), SE_DEBUG_NAME, &mut tps.Privileges[0].Luid) - .is_err() - { - return false; - } - - tps.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - if AdjustTokenPrivileges(token, FALSE.into(), Some(&tps), 0, None, None).is_err() { - return false; - } - - true - } -} - -fn get_pids() -> Vec { - let dword_size = size_of::(); - let mut pids: Vec = Vec::with_capacity(10192); - let mut cb_needed = 0; - - unsafe { - pids.set_len(10192); - let result = K32EnumProcesses( - pids.as_mut_ptr(), - (dword_size * pids.len()) as u32, - &mut cb_needed, - ); - if !result.as_bool() { - return Vec::new(); - } - let pids_len = cb_needed / dword_size as u32; - pids.set_len(pids_len as usize); - } - - pids.iter().map(|x| *x as i32).collect() -} - -fn get_ppid_threads() -> (HashMap, HashMap) { - let mut ppids = HashMap::new(); - let mut threads = HashMap::new(); - - unsafe { - let Ok(snapshot) = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) else { - return (ppids, threads); - }; - let mut entry: PROCESSENTRY32 = zeroed(); - entry.dwSize = size_of::() as u32; - let mut not_the_end = Process32First(snapshot, &mut entry); - - while not_the_end.is_ok() { - ppids.insert(entry.th32ProcessID as i32, entry.th32ParentProcessID as i32); - threads.insert(entry.th32ProcessID as i32, entry.cntThreads as i32); - not_the_end = Process32Next(snapshot, &mut entry); - } - - let _ = CloseHandle(snapshot); - } - - (ppids, threads) -} - -fn get_handle(pid: i32) -> Option { - if pid == 0 { - return None; - } - - let handle = unsafe { - OpenProcess( - PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, - FALSE.into(), - pid as u32, - ) - } - .ok(); - - match handle { - Some(h) if h.is_invalid() => None, - h => h, - } -} - -fn get_times(handle: HANDLE) -> Option<(u64, u64, u64, u64)> { - unsafe { - let mut start: FILETIME = zeroed(); - let mut exit: FILETIME = zeroed(); - let mut sys: FILETIME = zeroed(); - let mut user: FILETIME = zeroed(); - - let ret = GetProcessTimes( - handle, - &mut start as *mut FILETIME, - &mut exit as *mut FILETIME, - &mut sys as *mut FILETIME, - &mut user as *mut FILETIME, - ); - - let start = (u64::from(start.dwHighDateTime) << 32) | u64::from(start.dwLowDateTime); - let exit = (u64::from(exit.dwHighDateTime) << 32) | u64::from(exit.dwLowDateTime); - let sys = (u64::from(sys.dwHighDateTime) << 32) | u64::from(sys.dwLowDateTime); - let user = (u64::from(user.dwHighDateTime) << 32) | u64::from(user.dwLowDateTime); - - if ret.is_ok() { - Some((start, exit, sys, user)) - } else { - None - } - } -} - -fn get_memory_info(handle: HANDLE) -> Option { - unsafe { - let mut pmc: PROCESS_MEMORY_COUNTERS_EX = zeroed(); - let ret = GetProcessMemoryInfo( - handle, - &mut pmc as *mut PROCESS_MEMORY_COUNTERS_EX as *mut c_void - as *mut PROCESS_MEMORY_COUNTERS, - size_of::() as u32, - ); - - if ret.is_ok() { - let info = MemoryInfo { - page_fault_count: u64::from(pmc.PageFaultCount), - peak_working_set_size: pmc.PeakWorkingSetSize as u64, - working_set_size: pmc.WorkingSetSize as u64, - quota_peak_paged_pool_usage: pmc.QuotaPeakPagedPoolUsage as u64, - quota_paged_pool_usage: pmc.QuotaPagedPoolUsage as u64, - quota_peak_non_paged_pool_usage: pmc.QuotaPeakNonPagedPoolUsage as u64, - quota_non_paged_pool_usage: pmc.QuotaNonPagedPoolUsage as u64, - page_file_usage: pmc.PagefileUsage as u64, - peak_page_file_usage: pmc.PeakPagefileUsage as u64, - private_usage: pmc.PrivateUsage as u64, - }; - Some(info) - } else { - None - } - } -} - -fn get_command(handle: HANDLE) -> Option { - unsafe { - let mut exe_buf = [0u16; MAX_PATH as usize + 1]; - let h_mod = HMODULE::default(); - - let ret = GetModuleBaseNameW(handle, h_mod.into(), exe_buf.as_mut_slice()); - - let mut pos = 0; - for x in exe_buf.iter() { - if *x == 0 { - break; + LookupPrivilegeValueW(PCWSTR::null(), SE_DEBUG_NAME, &mut tps.Privileges[0].Luid).is_ok() + && { + tps.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + AdjustTokenPrivileges(*token, FALSE.into(), Some(&tps), 0, None, None).is_ok() } - pos += 1; - } - - if ret != 0 { - Some(String::from_utf16_lossy(&exe_buf[..pos])) - } else { - None - } } } @@ -434,6 +351,7 @@ trait RtlUserProcessParameters { fn get_cmdline(&self, handle: HANDLE) -> Result, &'static str>; fn get_cwd(&self, handle: HANDLE) -> Result, &'static str>; fn get_environ(&self, handle: HANDLE) -> Result, &'static str>; + fn process_group_id(&self) -> u32; } macro_rules! impl_RtlUserProcessParameters { @@ -456,6 +374,9 @@ macro_rules! impl_RtlUserProcessParameters { get_process_data(handle, ptr as _, size as _) } } + fn process_group_id(&self) -> u32 { + self.ProcessGroupId + } } }; } @@ -569,29 +490,39 @@ unsafe fn ph_query_process_variable_size( unsafe fn get_cmdline_from_buffer(buffer: PCWSTR) -> Vec { unsafe { + // `CommandLineToArgvW` returns the path of the current executable (nushell) for an empty + // command line. + if buffer.is_null() || *buffer.0 == 0 { + return Vec::new(); + } // Get argc and argv from the command line let mut argc = MaybeUninit::::uninit(); let argv_p = CommandLineToArgvW(buffer, argc.as_mut_ptr()); if argv_p.is_null() { return Vec::new(); } + // The array is ours to free with `LocalFree`, which dropping it does. + let _argv_memory = Owned::new(HLOCAL(argv_p.cast())); let argc = argc.assume_init(); let argv = std::slice::from_raw_parts(argv_p, argc as usize); - - let mut res = Vec::new(); - for arg in argv { - res.push(String::from_utf16_lossy(arg.as_wide())); - } - - let _err = LocalFree(HLOCAL(argv_p as _).into()); - - res + argv.iter() + .map(|arg| String::from_utf16_lossy(arg.as_wide())) + .collect() } } -unsafe fn get_process_params( - handle: HANDLE, -) -> Result<(Vec, Vec, PathBuf), &'static str> { +/// Fills in the command line, environment, working directory and process group from the +/// process's memory. Nothing is filled in when that memory can't be read. +/// +/// The process group is `RTL_USER_PROCESS_PARAMETERS.ProcessGroupId`, which Windows 8 added. +/// Rust's Windows targets need Windows 10, so it is always there. (The struct's `Length` counts +/// the strings stored after it, so it can't tell struct versions apart.) +/// +/// # Safety +/// +/// `handle` must be an open process handle with `PROCESS_QUERY_INFORMATION | PROCESS_VM_READ` +/// access. +unsafe fn read_process_params(handle: HANDLE, info: &mut ProcessInfo) -> Result<(), &'static str> { unsafe { if !cfg!(target_pointer_width = "64") { return Err("Non 64 bit targets are not supported"); @@ -657,12 +588,8 @@ unsafe fn get_process_params( return Err("Unable to read process parameters"); } - let proc_params = proc_params.assume_init(); - return Ok(( - get_cmd_line(&proc_params, handle), - get_proc_env(&proc_params, handle), - get_cwd(&proc_params, handle), - )); + fill_from_params(info, &proc_params.assume_init(), handle); + return Ok(()); } // target is a 32 bit process in wow64 mode @@ -692,15 +619,23 @@ unsafe fn get_process_params( { return Err("Unable to read 32 bit process parameters"); } - let proc_params = proc_params.assume_init(); - Ok(( - get_cmd_line(&proc_params, handle), - get_proc_env(&proc_params, handle), - get_cwd(&proc_params, handle), - )) + fill_from_params(info, &proc_params.assume_init(), handle); + Ok(()) } } +/// Fills in what `read_process_params` reads through a process's `RTL_USER_PROCESS_PARAMETERS`. +fn fill_from_params( + info: &mut ProcessInfo, + params: &T, + handle: HANDLE, +) { + info.command = command_line(&get_cmd_line(params, handle)); + info.environ = get_proc_env(params, handle); + info.cwd = get_cwd(params, handle); + info.process_group_id = Some(params.process_group_id() as i32); +} + static WINDOWS_8_1_OR_NEWER: LazyLock = LazyLock::new(|| unsafe { let mut version_info: OSVERSIONINFOEXW = MaybeUninit::zeroed().assume_init(); @@ -722,6 +657,16 @@ fn get_cmd_line(params: &T, handle: HANDLE) -> Vec< } } +/// Reads the command line without reading the process's memory, which only needs +/// `PROCESS_QUERY_LIMITED_INFORMATION`. Empty before Windows 8.1, which doesn't support it. +fn query_cmd_line(handle: HANDLE) -> Vec { + if *WINDOWS_8_1_OR_NEWER { + get_cmd_line_new(handle) + } else { + Vec::new() + } +} + #[allow(clippy::cast_ptr_alignment)] fn get_cmd_line_new(handle: HANDLE) -> Vec { unsafe { @@ -743,7 +688,7 @@ fn get_cmd_line_old(params: &T, handle: HANDLE) -> } } -fn get_proc_env(params: &T, handle: HANDLE) -> Vec { +fn get_proc_env(params: &T, handle: HANDLE) -> Option> { match params.get_environ(handle) { Ok(buffer) => { let equals = "=" @@ -766,51 +711,35 @@ fn get_proc_env(params: &T, handle: HANDLE) -> Vec< break; } } - result + Some(result) } - Err(_e) => Vec::new(), + Err(_e) => None, } } -fn get_cwd(params: &T, handle: HANDLE) -> PathBuf { +fn get_cwd(params: &T, handle: HANDLE) -> Option { match params.get_cwd(handle) { - Ok(buffer) => unsafe { PathBuf::from(null_terminated_wchar_to_string(buffer.as_slice())) }, - Err(_e) => PathBuf::new(), + Ok(buffer) => Some(unsafe { null_terminated_wchar_to_string(buffer.as_slice()) }), + Err(_e) => None, } } -fn get_io(handle: HANDLE) -> Option<(u64, u64)> { +/// Looks up the user a process runs as: its SID string (e.g. "S-1-5-18") and account name. +/// Account names are cached by SID in `user_names`, because looking one up can be slow (it may +/// ask a domain controller). +fn get_user( + handle: HANDLE, + user_names: &mut HashMap>, +) -> Option<(String, Option)> { unsafe { - let mut io: IO_COUNTERS = zeroed(); - let ret = GetProcessIoCounters(handle, &mut io); - - if ret.is_ok() { - Some((io.ReadTransferCount, io.WriteTransferCount)) - } else { - None - } - } -} - -#[derive(Clone)] -pub struct SidName { - pub sid: Vec, - pub name: Option, - pub domainname: Option, -} - -fn get_user(handle: HANDLE) -> Option { - unsafe { - let mut token: HANDLE = zeroed(); - let ret = OpenProcessToken(handle, TOKEN_QUERY, &mut token); - - if ret.is_err() { - return None; - } + let mut token = HANDLE::default(); + OpenProcessToken(handle, TOKEN_QUERY, &mut token).ok()?; + // The token closes when it drops. + let token = Owned::new(token); let mut cb_needed = 0; let _ = GetTokenInformation( - token, + *token, TokenUser, Some(ptr::null::() as *mut c_void), 0, @@ -819,141 +748,48 @@ fn get_user(handle: HANDLE) -> Option { let mut buf: Vec = Vec::with_capacity(cb_needed as usize); - let ret = GetTokenInformation( - token, + GetTokenInformation( + *token, TokenUser, Some(buf.as_mut_ptr() as *mut c_void), cb_needed, &mut cb_needed, - ); + ) + .ok()?; buf.set_len(cb_needed as usize); - if ret.is_err() { - return None; - } - #[allow(clippy::cast_ptr_alignment)] let token_user = buf.as_ptr() as *const TOKEN_USER; let psid = (*token_user).User.Sid; - let sid = get_sid(psid); - let (name, domainname) = if let Some((x, y)) = get_name_cached(psid) { - (Some(x), Some(y)) - } else { - (None, None) - }; + let sid = sid_string(psid)?; + let name = user_names + .entry(sid.clone()) + .or_insert_with(|| get_name(psid)) + .clone(); - Some(SidName { - sid, - name, - domainname, - }) + Some((sid, name)) } } -fn get_groups(handle: HANDLE) -> Option> { - unsafe { - let mut token: HANDLE = zeroed(); - let ret = OpenProcessToken(handle, TOKEN_QUERY, &mut token); - - if ret.is_err() { - return None; - } - - let mut cb_needed = 0; - let _ = GetTokenInformation( - token, - TokenGroups, - Some(ptr::null::() as *mut c_void), - 0, - &mut cb_needed, - ); - - let mut buf: Vec = Vec::with_capacity(cb_needed as usize); - - let ret = GetTokenInformation( - token, - TokenGroups, - Some(buf.as_mut_ptr() as *mut c_void), - cb_needed, - &mut cb_needed, - ); - buf.set_len(cb_needed as usize); - - if ret.is_err() { - return None; - } - - #[allow(clippy::cast_ptr_alignment)] - let token_groups = buf.as_ptr() as *const TOKEN_GROUPS; - - let mut ret = Vec::new(); - let sa = (*token_groups).Groups.as_ptr(); - for i in 0..(*token_groups).GroupCount { - let psid = (*sa.offset(i as isize)).Sid; - let sid = get_sid(psid); - let (name, domainname) = if let Some((x, y)) = get_name_cached(psid) { - (Some(x), Some(y)) - } else { - (None, None) - }; - - let sid_name = SidName { - sid, - name, - domainname, - }; - ret.push(sid_name); - } - - Some(ret) - } +/// Formats a SID as a string such as "S-1-5-18". +/// +/// # Safety +/// +/// `psid` must point to a valid SID. +unsafe fn sid_string(psid: PSID) -> Option { + let mut string = PWSTR::null(); + // SAFETY: the caller guarantees that `psid` is valid. On success `string` points to a + // NUL-terminated string that the system allocated with `LocalAlloc`. + unsafe { ConvertSidToStringSidW(psid, &mut string) }.ok()?; + // SAFETY: the string is ours to free with `LocalFree`, which dropping this does. + let _string_memory = unsafe { Owned::new(HLOCAL(string.0.cast())) }; + // SAFETY: `string` stays allocated until `_string_memory` drops. + unsafe { string.to_string() }.ok() } -fn get_sid(psid: PSID) -> Vec { - unsafe { - let mut ret = Vec::new(); - let psid = psid.0 as *const SID; - - let mut ia = 0; - ia |= u64::from((*psid).IdentifierAuthority.Value[0]) << 40; - ia |= u64::from((*psid).IdentifierAuthority.Value[1]) << 32; - ia |= u64::from((*psid).IdentifierAuthority.Value[2]) << 24; - ia |= u64::from((*psid).IdentifierAuthority.Value[3]) << 16; - ia |= u64::from((*psid).IdentifierAuthority.Value[4]) << 8; - ia |= u64::from((*psid).IdentifierAuthority.Value[5]); - - ret.push(u64::from((*psid).Revision)); - ret.push(ia); - let cnt = (*psid).SubAuthorityCount; - let sa = (*psid).SubAuthority.as_ptr(); - for i in 0..cnt { - ret.push(u64::from(*sa.offset(i as isize))); - } - - ret - } -} - -thread_local!( - pub static NAME_CACHE: RefCell>> = - RefCell::new(HashMap::new()); -); - -fn get_name_cached(psid: PSID) -> Option<(String, String)> { - NAME_CACHE.with(|c| { - let mut c = c.borrow_mut(); - if let Some(x) = c.get(&psid.0) { - x.clone() - } else { - let x = get_name(psid); - c.insert(psid.0, x.clone()); - x - } - }) -} - -fn get_name(psid: PSID) -> Option<(String, String)> { +/// Looks up the account name of a SID. +fn get_name(psid: PSID) -> Option { unsafe { let mut cc_name = 0; let mut cc_domainname = 0; @@ -972,11 +808,9 @@ fn get_name(psid: PSID) -> Option<(String, String)> { return None; } - let mut name: Vec = Vec::with_capacity(cc_name as usize); - let mut domainname: Vec = Vec::with_capacity(cc_domainname as usize); - name.set_len(cc_name as usize); - domainname.set_len(cc_domainname as usize); - if LookupAccountSidW( + let mut name = vec![0u16; cc_name as usize]; + let mut domainname = vec![0u16; cc_domainname as usize]; + LookupAccountSidW( None, psid, PWSTR::from_raw(name.as_mut_ptr()).into(), @@ -985,93 +819,40 @@ fn get_name(psid: PSID) -> Option<(String, String)> { &mut cc_domainname, &mut pe_use, ) - .is_err() - { - return None; - } + .ok()?; - let name = from_wide_ptr(name.as_ptr()); - let domainname = from_wide_ptr(domainname.as_ptr()); - Some((name, domainname)) + // On success `cc_name` is the length of the name, not counting the terminating NUL. + Some(String::from_utf16_lossy(name.get(..cc_name as usize)?)) } } -fn from_wide_ptr(ptr: *const u16) -> String { - unsafe { - assert!(!ptr.is_null()); - let len = (0..isize::MAX) - .position(|i| *ptr.offset(i) == 0) - .unwrap_or_default(); - let slice = std::slice::from_raw_parts(ptr, len); - OsString::from_wide(slice).to_string_lossy().into_owned() +/// Maps a process's base priority to the nice value libuv gives its priority class: realtime +/// -20, high -14, above normal -7, normal 0, below normal 10 and idle 19. Each class has a fixed +/// base priority (24, 13, 10, 8, 6 and 4), so ranges between them keep a process whose base +/// priority was set directly in the nearest class. +fn nice_from_base_priority(base_priority: i64) -> i64 { + match base_priority { + ..=4 => 19, + 5..=6 => 10, + 7..=9 => 0, + 10..=11 => -7, + 12..=15 => -14, + _ => -20, } } -fn get_priority(handle: HANDLE) -> u32 { - unsafe { GetPriorityClass(handle) } -} +#[cfg(test)] +mod tests { + use super::nice_from_base_priority; -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.command.clone() - } - - /// Full name of command, with arguments - pub fn command(&self) -> String { - self.cmd.join(" ") - } - - pub fn environ(&self) -> Vec { - self.environ.clone() - } - - pub fn cwd(&self) -> String { - self.cwd.display().to_string() - } - - /// Get the status of the process - pub fn status(&self) -> String { - "unknown".to_string() - } - - /// CPU usage as a percent of total - pub fn cpu_usage(&self) -> f64 { - let curr_time = self.cpu_info.curr_sys + self.cpu_info.curr_user; - let prev_time = self.cpu_info.prev_sys + self.cpu_info.prev_user; - - let usage_ms = curr_time.saturating_sub(prev_time) / 10000u64; - let interval_ms = self.interval.as_secs() * 1000 + u64::from(self.interval.subsec_millis()); - usage_ms as f64 * 100.0 / interval_ms as f64 - } - - /// Memory size in number of bytes - pub fn mem_size(&self) -> u64 { - self.memory_info.working_set_size - } - - /// Working set size in bytes - pub fn working_size(&self) -> u64 { - self.memory_info.working_set_size - } - - /// Paged memory size in bytes - pub fn paged_size(&self) -> u64 { - self.memory_info.page_file_usage - } - - /// Virtual memory size in bytes - pub fn virtual_size(&self) -> u64 { - self.memory_info.private_usage + #[test] + fn priority_classes_map_to_libuv_nice_values() { + // Base priorities of the idle, below normal, normal, above normal, high and realtime + // priority classes. + let nice: Vec = [4, 6, 8, 10, 13, 24] + .into_iter() + .map(nice_from_base_priority) + .collect(); + assert_eq!(nice, [19, 10, 0, -7, -14, -20]); } }