bug: add bindings to grab ppid in some cases on macos (#825)

This commit is contained in:
Clement Tsang 2022-10-11 19:49:39 -04:00 committed by GitHub
parent e7b31dfb96
commit 1e5f0ea2d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 393 additions and 26 deletions

10
Cargo.lock generated
View file

@ -227,6 +227,7 @@ dependencies = [
"itertools", "itertools",
"libc", "libc",
"log", "log",
"mach2",
"nvml-wrapper", "nvml-wrapper",
"once_cell", "once_cell",
"predicates", "predicates",
@ -967,6 +968,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "mach2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.4.1" version = "2.4.1"

View file

@ -61,7 +61,6 @@ concat-string = "1.0.1"
crossterm = "0.18.2" crossterm = "0.18.2"
ctrlc = { version = "3.1.9", features = ["termination"] } ctrlc = { version = "3.1.9", features = ["termination"] }
dirs = "4.0.0" dirs = "4.0.0"
fern = { version = "0.6.1", optional = true } fern = { version = "0.6.1", optional = true }
futures = "0.3.21" futures = "0.3.21"
futures-timer = "3.0.2" futures-timer = "3.0.2"
@ -93,6 +92,7 @@ smol = "1.2.5"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
heim = { version = "0.1.0-rc.1", features = ["cpu", "disk", "memory", "net"] } heim = { version = "0.1.0-rc.1", features = ["cpu", "disk", "memory", "net"] }
mach2 = "0.4.1"
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
heim = { version = "0.1.0-rc.1", features = ["cpu", "disk", "memory"] } heim = { version = "0.1.0-rc.1", features = ["cpu", "disk", "memory"] }
@ -114,9 +114,21 @@ clap_mangen = "0.1.6"
[package.metadata.deb] [package.metadata.deb]
section = "utility" section = "utility"
assets = [ assets = [
["target/release/btm", "usr/bin/", "755"], [
["LICENSE", "usr/share/doc/btm/", "644"], "target/release/btm",
["manpage/btm.1.gz", "usr/share/man/man1/btm.1.gz", "644"], "usr/bin/",
"755",
],
[
"LICENSE",
"usr/share/doc/btm/",
"644",
],
[
"manpage/btm.1.gz",
"usr/share/man/man1/btm.1.gz",
"644",
],
[ [
"completion/btm.bash", "completion/btm.bash",
"usr/share/bash-completion/completions/btm", "usr/share/bash-completion/completions/btm",
@ -127,7 +139,11 @@ assets = [
"usr/share/fish/vendor_completions.d/btm.fish", "usr/share/fish/vendor_completions.d/btm.fish",
"644", "644",
], ],
["completion/_btm", "usr/share/zsh/vendor-completions/", "644"], [
"completion/_btm",
"usr/share/zsh/vendor-completions/",
"644",
],
] ]
extended-description = """\ extended-description = """\
A customizable cross-platform graphical process/system monitor for the terminal. Supports Linux, macOS, and Windows. A customizable cross-platform graphical process/system monitor for the terminal. Supports Linux, macOS, and Windows.

View file

@ -110,21 +110,17 @@ impl ProcessData {
.collect(); .collect();
self.process_harvest = process_pid_map; self.process_harvest = process_pid_map;
// This also needs a quick sort + reverse to be in the correct order. // We collect all processes that either:
// - Do not have a parent PID (that is, they are orphan processes)
// - Have a parent PID but we don't have the parent (we promote them as orphans)
// Note this also needs a quick sort + reverse to be in the correct order.
self.orphan_pids = { self.orphan_pids = {
let mut res: Vec<Pid> = self let mut res: Vec<Pid> = self
.process_harvest .process_harvest
.iter() .iter()
.filter_map(|(pid, process_harvest)| { .filter_map(|(pid, process_harvest)| match process_harvest.parent_pid {
if let Some(parent_pid) = process_harvest.parent_pid { Some(parent_pid) if self.process_harvest.contains_key(&parent_pid) => None,
if self.process_harvest.contains_key(&parent_pid) { _ => Some(*pid),
None
} else {
Some(*pid)
}
} else {
Some(*pid)
}
}) })
.sorted() .sorted()
.collect(); .collect();

View file

@ -1,9 +1,10 @@
//! Process data collection for macOS. Uses sysinfo. //! Process data collection for macOS. Uses sysinfo and custom bindings.
use super::ProcessHarvest; use super::ProcessHarvest;
use sysinfo::System; use sysinfo::System;
use crate::data_harvester::processes::UserTable; use crate::{data_harvester::processes::UserTable, Pid};
mod sysctl_bindings;
pub fn get_process_data( pub fn get_process_data(
sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, user_table: &mut UserTable, sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, user_table: &mut UserTable,
@ -17,8 +18,14 @@ pub fn get_process_data(
) )
} }
pub(crate) fn fallback_macos_ppid(pid: Pid) -> Option<Pid> {
sysctl_bindings::kinfo_process(pid)
.map(|kinfo| kinfo.kp_eproc.e_ppid)
.ok()
}
fn get_macos_process_cpu_usage( fn get_macos_process_cpu_usage(
pids: &[i32], pids: &[Pid],
) -> std::io::Result<std::collections::HashMap<i32, f64>> { ) -> std::io::Result<std::collections::HashMap<i32, f64>> {
use itertools::Itertools; use itertools::Itertools;
let output = std::process::Command::new("ps") let output = std::process::Command::new("ps")

View file

@ -0,0 +1,322 @@
//! Partial bindings from Apple's open source code for getting process information.
//! Some of this is based on [heim's binding implementation](https://github.com/heim-rs/heim/blob/master/heim-process/src/sys/macos/bindings/process.rs).
use std::mem;
use anyhow::{bail, Result};
use libc::{
boolean_t, c_char, c_long, c_short, c_uchar, c_ushort, c_void, dev_t, gid_t, itimerval, pid_t,
rusage, sigset_t, timeval, uid_t, xucred, CTL_KERN, KERN_PROC, KERN_PROC_PID, MAXCOMLEN,
};
use mach2::vm_types::user_addr_t;
use crate::Pid;
#[allow(non_camel_case_types)]
#[repr(C)]
pub(crate) struct kinfo_proc {
pub kp_proc: extern_proc,
pub kp_eproc: eproc,
}
#[allow(non_camel_case_types)]
#[repr(C)]
#[derive(Copy, Clone)]
pub struct p_st1 {
/// Doubly-linked run/sleep queue.
p_forw: user_addr_t,
p_back: user_addr_t,
}
#[allow(non_camel_case_types)]
#[repr(C)]
pub union p_un {
pub p_st1: p_st1,
/// process start time
pub p_starttime: timeval,
}
/// Exported fields for kern sysctl. See
/// [`proc.h`](https://opensource.apple.com/source/xnu/xnu-201/bsd/sys/proc.h)
#[allow(non_camel_case_types)]
#[repr(C)]
pub(crate) struct extern_proc {
pub p_un: p_un,
/// Address space.
pub p_vmspace: *mut vmspace,
/// Signal actions, state (PROC ONLY). Should point to
/// a `sigacts` but we don't really seem to need this.
pub p_sigacts: user_addr_t,
/// P_* flags.
pub p_flag: i32,
/// S* process status.
pub p_stat: c_char,
/// Process identifier.
pub p_pid: pid_t,
/// Save parent pid during ptrace. XXX
pub p_oppid: pid_t,
/// Sideways return value from fdopen. XXX
pub p_dupfd: i32,
/// where user stack was allocated
pub user_stack: caddr_t,
/// XXX Which thread is exiting?
pub exit_thread: *mut c_void,
/// allow to debug
pub p_debugger: i32,
/// indication to suspend
pub sigwait: boolean_t,
/// Time averaged value of p_cpticks.
pub p_estcpu: u32,
/// Ticks of cpu time.
pub p_cpticks: i32,
/// %cpu for this process during p_swtime
pub p_pctcpu: fixpt_t,
/// Sleep address.
pub p_wchan: *mut c_void,
/// Reason for sleep.
pub p_wmesg: *mut c_char,
/// Time swapped in or out.
pub p_swtime: u32,
/// Time since last blocked.
pub p_slptime: u32,
/// Alarm timer.
pub p_realtimer: itimerval,
/// Real time.
pub p_rtime: timeval,
/// Statclock hit in user mode.
pub p_uticks: u64,
/// Statclock hits in system mode.
pub p_sticks: u64,
/// Statclock hits processing intr.
pub p_iticks: u64,
/// Kernel trace points.
pub p_traceflag: i32,
/// Trace to vnode. Originally a pointer to a struct of vnode.
pub p_tracep: *mut c_void,
/// DEPRECATED.
pub p_siglist: i32,
/// Vnode of executable. Originally a pointer to a struct of vnode.
pub p_textvp: *mut c_void,
/// If non-zero, don't swap.
pub p_holdcnt: i32,
/// DEPRECATED.
pub p_sigmask: sigset_t,
/// Signals being ignored.
pub p_sigignore: sigset_t,
/// Signals being caught by user.
pub p_sigcatch: sigset_t,
/// Process priority.
pub p_priority: c_uchar,
/// User-priority based on p_cpu and p_nice.
pub p_usrpri: c_uchar,
/// Process "nice" value.
pub p_nice: c_char,
pub p_comm: [c_char; MAXCOMLEN + 1],
/// Pointer to process group. Originally a pointer to a `pgrp`.
pub p_pgrp: *mut c_void,
/// Kernel virtual addr of u-area (PROC ONLY). Originally a pointer to a `user`.
pub p_addr: *mut c_void,
/// Exit status for wait; also stop signal.
pub p_xstat: c_ushort,
/// Accounting flags.
pub p_acflag: c_ushort,
/// Exit information. XXX
pub p_ru: *mut rusage,
}
const WMESGLEN: usize = 7;
const COMAPT_MAXLOGNAME: usize = 12;
/// See `_caddr_t.h`.
#[allow(non_camel_case_types)]
type caddr_t = *const libc::c_char;
/// See `types.h`.
#[allow(non_camel_case_types)]
type segsz_t = i32;
/// See `types.h`.
#[allow(non_camel_case_types)]
type fixpt_t = u32;
/// See [`proc.h`](https://opensource.apple.com/source/xnu/xnu-201/bsd/sys/proc.h)
#[allow(non_camel_case_types)]
#[repr(C)]
pub(crate) struct pcred {
pub pc_lock: [c_char; 72],
pub pc_ucred: *mut xucred,
pub p_ruid: uid_t,
pub p_svuid: uid_t,
pub p_rgid: gid_t,
pub p_svgid: gid_t,
pub p_refcnt: i32,
}
/// See `vm.h`.
#[allow(non_camel_case_types)]
#[repr(C)]
pub(crate) struct vmspace {
pub dummy: i32,
pub dummy2: caddr_t,
pub dummy3: [i32; 5],
pub dummy4: [caddr_t; 3],
}
/// See [`sysctl.h`](https://opensource.apple.com/source/xnu/xnu-344/bsd/sys/sysctl.h).
#[allow(non_camel_case_types)]
#[repr(C)]
pub(crate) struct eproc {
/// Address of proc. We just cheat and use a c_void pointer since we aren't using this.
pub e_paddr: *mut c_void,
/// Session pointer. We just cheat and use a c_void pointer since we aren't using this.
pub e_sess: *mut c_void,
/// Process credentials
pub e_pcred: pcred,
/// Current credentials
pub e_ucred: xucred,
/// Address space
pub e_vm: vmspace,
/// Parent process ID
pub e_ppid: pid_t,
/// Process group ID
pub e_pgid: pid_t,
/// Job control counter
pub e_jobc: c_short,
/// Controlling tty dev
pub e_tdev: dev_t,
/// tty process group id
pub e_tpgid: pid_t,
/// tty session pointer. We just cheat and use a c_void pointer since we aren't using this.
pub e_tsess: *mut c_void,
/// wchan message
pub e_wmesg: [c_char; WMESGLEN + 1],
/// text size
pub e_xsize: segsz_t,
/// text rss
pub e_xrssize: c_short,
/// text references
pub e_xccount: c_short,
pub e_xswrss: c_short,
pub e_flag: c_long,
/// short setlogin() name
pub e_login: [c_char; COMAPT_MAXLOGNAME],
pub e_spare: [c_long; 4],
}
/// Obtains the [`kinfo_proc`] given a process PID.
///
/// From [heim](https://github.com/heim-rs/heim/blob/master/heim-process/src/sys/macos/bindings/process.rs#L235).
pub(crate) fn kinfo_process(pid: Pid) -> Result<kinfo_proc> {
let mut name: [i32; 4] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid];
let mut size = mem::size_of::<kinfo_proc>();
let mut info = mem::MaybeUninit::<kinfo_proc>::uninit();
let result = unsafe {
libc::sysctl(
name.as_mut_ptr(),
4,
info.as_mut_ptr() as *mut libc::c_void,
&mut size,
std::ptr::null_mut(),
0,
)
};
if result < 0 {
bail!("failed to get process for pid {pid}");
}
// sysctl succeeds but size is zero, happens when process has gone away
if size == 0 {
bail!("failed to get process for pid {pid}");
}
unsafe { Ok(info.assume_init()) }
}
#[cfg(test)]
mod test {
use super::*;
use std::mem;
/// A quick test to ensure that things are sized correctly.
#[test]
fn test_struct_sizes() {
assert_eq!(mem::size_of::<p_st1>(), 16);
assert_eq!(mem::align_of::<p_st1>(), 8);
assert_eq!(mem::size_of::<pcred>(), 104);
assert_eq!(mem::align_of::<pcred>(), 8);
assert_eq!(mem::size_of::<vmspace>(), 64);
assert_eq!(mem::align_of::<vmspace>(), 8);
assert_eq!(mem::size_of::<extern_proc>(), 296);
assert_eq!(mem::align_of::<extern_proc>(), 8);
assert_eq!(mem::size_of::<eproc>(), 376);
assert_eq!(mem::align_of::<eproc>(), 8);
assert_eq!(mem::size_of::<kinfo_proc>(), 672);
assert_eq!(mem::align_of::<kinfo_proc>(), 8);
}
}

View file

@ -6,12 +6,15 @@ use std::io;
use super::ProcessHarvest; use super::ProcessHarvest;
use sysinfo::{CpuExt, PidExt, ProcessExt, ProcessStatus, System, SystemExt}; use sysinfo::{CpuExt, PidExt, ProcessExt, ProcessStatus, System, SystemExt};
use crate::data_harvester::processes::UserTable; use crate::{data_harvester::processes::UserTable, utils::error::Result, Pid};
pub fn get_process_data( pub fn get_process_data<F>(
sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, user_table: &mut UserTable, sys: &System, use_current_cpu_total: bool, mem_total_kb: u64, user_table: &mut UserTable,
get_process_cpu_usage: impl Fn(&[i32]) -> io::Result<HashMap<i32, f64>>, get_process_cpu_usage: F,
) -> crate::utils::error::Result<Vec<ProcessHarvest>> { ) -> Result<Vec<ProcessHarvest>>
where
F: Fn(&[Pid]) -> io::Result<HashMap<Pid, f64>>,
{
let mut process_vector: Vec<ProcessHarvest> = Vec::new(); let mut process_vector: Vec<ProcessHarvest> = Vec::new();
let process_hashmap = sys.processes(); let process_hashmap = sys.processes();
let cpu_usage = sys.global_cpu_info().cpu_usage() as f64 / 100.0; let cpu_usage = sys.global_cpu_info().cpu_usage() as f64 / 100.0;
@ -66,9 +69,22 @@ pub fn get_process_data(
(ps.to_string(), convert_process_status_to_char(ps)) (ps.to_string(), convert_process_status_to_char(ps))
}; };
let uid = process_val.user_id().map(|u| **u); let uid = process_val.user_id().map(|u| **u);
let pid = process_val.pid().as_u32() as Pid;
process_vector.push(ProcessHarvest { process_vector.push(ProcessHarvest {
pid: process_val.pid().as_u32() as _, pid,
parent_pid: process_val.parent().map(|p| p.as_u32() as _), parent_pid: {
#[cfg(target_os = "macos")]
{
process_val
.parent()
.map(|p| p.as_u32() as _)
.or_else(|| super::fallback_macos_ppid(pid))
}
#[cfg(not(target_os = "macos"))]
{
process_val.parent().map(|p| p.as_u32() as _)
}
},
name, name,
command, command,
mem_usage_percent: if mem_total_kb > 0 { mem_usage_percent: if mem_total_kb > 0 {
@ -96,7 +112,7 @@ pub fn get_process_data(
} }
let unknown_state = ProcessStatus::Unknown(0).to_string(); let unknown_state = ProcessStatus::Unknown(0).to_string();
let cpu_usage_unknown_pids: Vec<i32> = process_vector let cpu_usage_unknown_pids: Vec<Pid> = process_vector
.iter() .iter()
.filter(|process| process.process_state.0 == unknown_state) .filter(|process| process.process_state.0 == unknown_state)
.map(|process| process.pid) .map(|process| process.pid)