use crate::Pid;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ProcessTarget {
pub pid: Pid,
pub starttime: u64,
}
impl From<&crate::row::Row> for ProcessTarget {
fn from(row: &crate::row::Row) -> Self {
Self {
pid: row.pid,
starttime: row.starttime,
}
}
}
impl ProcessTarget {
pub fn capture(pid: Pid) -> std::io::Result<Self> {
valid_pid(pid)?;
Ok(Self {
pid,
starttime: current_starttime(pid)?,
})
}
pub fn verify(self) -> std::io::Result<()> {
let current = Self::capture(self.pid)?;
if current == self {
Ok(())
} else {
Err(std::io::Error::other(
"process identity changed; refusing operation",
))
}
}
}
fn valid_pid(pid: Pid) -> std::io::Result<()> {
if pid == 0 || pid > i32::MAX as u32 {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"invalid process PID",
))
} else {
Ok(())
}
}
#[cfg(target_os = "linux")]
fn current_starttime(pid: Pid) -> std::io::Result<u64> {
crate::linux::scan::read_stat(std::path::Path::new("/proc"), pid)
.map(|(_, stat)| stat.starttime)
.ok_or_else(|| std::io::Error::other("cannot verify process start time"))
}
#[cfg(target_os = "macos")]
fn current_starttime(pid: Pid) -> std::io::Result<u64> {
crate::darwin::kinfo::list_processes()?
.into_iter()
.find(|p| p.pid == pid)
.map(|p| {
p.start_sec
.saturating_mul(1_000_000)
.saturating_add(p.start_usec)
})
.ok_or_else(|| std::io::Error::other("cannot verify process start time"))
}
#[cfg(target_os = "freebsd")]
fn current_starttime(pid: Pid) -> std::io::Result<u64> {
crate::freebsd::scan::process(pid)
.map(|p| {
(p.start_sec.max(0) as u64)
.saturating_mul(1_000_000)
.saturating_add(p.start_usec.max(0) as u64)
})
.ok_or_else(|| std::io::Error::other("cannot verify process start time"))
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
fn current_starttime(_pid: Pid) -> std::io::Result<u64> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"cannot verify process identity",
))
}
pub fn execute_checked(target: ProcessTarget, action: &Action) -> std::io::Result<()> {
let pid = match *action {
Action::Kill { pid, .. } | Action::Renice { pid, .. } | Action::Priority { pid, .. } => pid,
};
valid_pid(pid)?;
if pid != target.pid {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"action target mismatch",
));
}
#[cfg(target_os = "linux")]
if let Action::Kill { signal, .. } = *action {
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
if fd >= 0 {
let fd = unsafe { OwnedFd::from_raw_fd(fd as i32) };
target.verify()?;
let rc = unsafe {
libc::syscall(
libc::SYS_pidfd_send_signal,
fd.as_raw_fd(),
signal,
std::ptr::null::<libc::siginfo_t>(),
0,
)
};
return if rc == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
};
}
let error = std::io::Error::last_os_error();
if !matches!(
error.raw_os_error(),
Some(libc::ENOSYS) | Some(libc::EINVAL) | Some(libc::ENOENT)
) {
return Err(error);
}
}
target.verify()?;
execute(action)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Action {
Kill {
pid: Pid,
signal: i32,
},
Renice {
pid: Pid,
nice: i8,
},
Priority {
pid: Pid,
delta: i8,
},
}
pub const SIGNALS: &[(i32, &str)] = &[
(libc::SIGHUP, "SIGHUP"),
(libc::SIGINT, "SIGINT"),
(libc::SIGQUIT, "SIGQUIT"),
(libc::SIGILL, "SIGILL"),
(libc::SIGABRT, "SIGABRT"),
(libc::SIGKILL, "SIGKILL"),
(libc::SIGSEGV, "SIGSEGV"),
(libc::SIGTERM, "SIGTERM"),
(libc::SIGCHLD, "SIGCHLD"),
(libc::SIGCONT, "SIGCONT"),
(libc::SIGSTOP, "SIGSTOP"),
(libc::SIGTSTP, "SIGTSTP"),
];
#[cfg(unix)]
pub fn execute(action: &Action) -> Result<(), std::io::Error> {
use std::io;
let pid = match *action {
Action::Kill { pid, .. } | Action::Renice { pid, .. } | Action::Priority { pid, .. } => pid,
};
valid_pid(pid)?;
match *action {
Action::Kill { pid, signal } => {
let rc = unsafe { libc::kill(pid as libc::pid_t, signal as libc::c_int) };
if rc == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
Action::Renice { pid, nice } => set_nice(pid, nice as i32),
Action::Priority { pid, delta } => {
unsafe { set_errno(0) };
let cur = unsafe { libc::getpriority(libc::PRIO_PROCESS, who(pid)) };
if cur == -1 && unsafe { errno() } != 0 {
return Err(io::Error::last_os_error());
}
let target = (cur + delta as i32).clamp(-20, 19);
set_nice(pid, target)
}
}
}
#[cfg(unix)]
unsafe fn errno() -> i32 {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
*libc::__errno_location()
}
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
{
*libc::__error()
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "ios",
target_os = "freebsd"
)))]
{
0
}
}
#[cfg(unix)]
unsafe fn set_errno(value: i32) {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
*libc::__errno_location() = value;
}
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
{
*libc::__error() = value;
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "ios",
target_os = "freebsd"
)))]
{
let _ = value;
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn who(pid: Pid) -> libc::id_t {
pid as libc::id_t
}
#[cfg(target_os = "freebsd")]
fn who(pid: Pid) -> libc::c_int {
pid as libc::c_int
}
#[cfg(unix)]
fn set_nice(pid: Pid, nice: i32) -> Result<(), std::io::Error> {
use std::io;
let rc = unsafe { libc::setpriority(libc::PRIO_PROCESS, who(pid), nice) };
if rc == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
#[cfg(not(unix))]
pub fn execute(_action: &Action) -> Result<(), std::io::Error> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"process actions require a unix target",
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn actions_are_comparable() {
assert_eq!(
Action::Kill { pid: 1, signal: 9 },
Action::Kill { pid: 1, signal: 9 }
);
assert_ne!(
Action::Kill { pid: 1, signal: 9 },
Action::Kill { pid: 1, signal: 15 }
);
}
#[test]
fn signal_table_is_unique_and_contains_common_signals() {
let mut signals: Vec<i32> = SIGNALS.iter().map(|(n, _)| *n).collect();
let len = signals.len();
signals.sort_unstable();
signals.dedup();
assert_eq!(signals.len(), len, "duplicate signal numbers");
assert_eq!(signals.first(), Some(&1));
assert!(signals.contains(&9) && signals.contains(&15));
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum IoClass {
None,
Realtime,
BestEffort,
Idle,
}
impl IoClass {
pub fn bits(self) -> u32 {
match self {
IoClass::None => 0,
IoClass::Realtime => 1,
IoClass::BestEffort => 2,
IoClass::Idle => 3,
}
}
pub fn from_bits(bits: u32) -> Option<Self> {
Some(match bits {
0 => IoClass::None,
1 => IoClass::Realtime,
2 => IoClass::BestEffort,
3 => IoClass::Idle,
_ => return None,
})
}
pub fn name(self) -> &'static str {
match self {
IoClass::None => "none",
IoClass::Realtime => "realtime",
IoClass::BestEffort => "best-effort",
IoClass::Idle => "idle",
}
}
pub fn next(self) -> Self {
match self {
IoClass::None => IoClass::BestEffort,
IoClass::BestEffort => IoClass::Idle,
IoClass::Idle => IoClass::Realtime,
IoClass::Realtime => IoClass::None,
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub struct IoPriority {
pub class: IoClass,
pub level: u8,
}
impl Default for IoPriority {
fn default() -> Self {
Self {
class: IoClass::None,
level: 0,
}
}
}
impl IoPriority {
pub fn new(class: IoClass, level: u8) -> Self {
Self {
class,
level: level.min(7),
}
}
pub fn to_bits(self) -> i32 {
(((self.class.bits() as i32) << 13) | self.level as i32) & 0xFFFF
}
pub fn from_bits(bits: i32) -> Self {
let class = IoClass::from_bits(((bits >> 13) & 0x7) as u32).unwrap_or(IoClass::None);
Self {
class,
level: (bits & 0x7) as u8,
}
}
pub fn label(self) -> String {
format!("{}/{}", self.class.name(), self.level)
}
}
pub fn ioprio_supported() -> bool {
cfg!(target_os = "linux")
}
#[cfg(target_os = "linux")]
pub fn ioprio_get(pid: Pid) -> Result<IoPriority, std::io::Error> {
use std::io;
let rc = unsafe {
libc::syscall(libc::SYS_ioprio_get, 1 , pid)
};
if rc < 0 {
return Err(io::Error::last_os_error());
}
Ok(IoPriority::from_bits(rc as i32))
}
#[cfg(not(target_os = "linux"))]
pub fn ioprio_get(_pid: Pid) -> Result<IoPriority, std::io::Error> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"I/O priority is a Linux feature (ioprio_get)",
))
}
#[cfg(target_os = "linux")]
pub fn ioprio_set(pid: Pid, priority: IoPriority) -> Result<(), std::io::Error> {
use std::io;
let rc = unsafe {
libc::syscall(
libc::SYS_ioprio_set,
1,
pid,
priority.to_bits(),
)
};
if rc < 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(not(target_os = "linux"))]
pub fn ioprio_set(_pid: Pid, _priority: IoPriority) -> Result<(), std::io::Error> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"I/O priority is a Linux feature (ioprio_set)",
))
}
#[cfg(test)]
mod ioprio_tests {
use super::*;
#[test]
fn bits_pack_and_unpack() {
for class in [
IoClass::None,
IoClass::Realtime,
IoClass::BestEffort,
IoClass::Idle,
] {
for level in 0..=7u8 {
let p = IoPriority::new(class, level);
let back = IoPriority::from_bits(p.to_bits());
assert_eq!(back, p, "{:?}/{}", class, level);
}
}
}
#[test]
fn the_default_is_class_none() {
let p = IoPriority::default();
assert_eq!(p.class, IoClass::None);
assert_eq!(p.level, 0);
assert_eq!(p.to_bits(), 0);
}
#[test]
fn levels_are_clamped_to_seven() {
let p = IoPriority::new(IoClass::BestEffort, 200);
assert_eq!(p.level, 7);
}
#[test]
fn classes_cycle_through_every_variant() {
let mut c = IoClass::None;
let mut seen = vec![c];
for _ in 0..3 {
c = c.next();
seen.push(c);
}
assert_eq!(seen.len(), 4);
assert_eq!(c.next(), IoClass::None, "the cycle closes");
}
#[test]
fn labels_are_readable() {
let p = IoPriority::new(IoClass::BestEffort, 4);
assert_eq!(p.label(), "best-effort/4");
}
#[test]
fn an_out_of_range_class_degrades_to_none() {
let p = IoPriority::from_bits((7 << 13) | 3);
assert_eq!(p.class, IoClass::None);
assert_eq!(p.level, 3);
}
#[cfg(target_os = "linux")]
#[test]
fn reading_our_own_ioprio_works() {
let p = ioprio_get(std::process::id()).expect("ioprio_get on self");
assert!(matches!(
p.class,
IoClass::None | IoClass::Realtime | IoClass::BestEffort | IoClass::Idle
));
}
#[cfg(target_os = "linux")]
#[test]
fn setting_our_own_ioprio_round_trips() {
let original = ioprio_get(std::process::id()).unwrap();
let target = IoPriority::new(IoClass::BestEffort, 6);
if ioprio_set(std::process::id(), target).is_ok() {
assert_eq!(ioprio_get(std::process::id()).unwrap(), target);
}
let _ = ioprio_set(std::process::id(), original);
}
#[cfg(not(target_os = "linux"))]
#[test]
fn unsupported_platforms_explain_themselves() {
assert!(!ioprio_supported());
let err = ioprio_get(std::process::id()).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::Unsupported);
assert!(err.to_string().contains("Linux"));
}
}