use crate::error::CoreError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Signal {
Term,
Kill,
}
impl Signal {
fn as_libc(self) -> i32 {
match self {
Signal::Term => libc::SIGTERM,
Signal::Kill => libc::SIGKILL,
}
}
}
pub fn kill_process(pid: u32, signal: Signal) -> Result<(), CoreError> {
let pid_i32 = i32::try_from(pid).map_err(|_| CoreError::ProcessNotFound { pid })?;
if pid_i32 <= 0 {
return Err(CoreError::ProcessNotFound { pid });
}
let ret = unsafe { libc::kill(pid_i32, signal.as_libc()) };
if ret == 0 {
return Ok(());
}
let err = std::io::Error::last_os_error();
match err.raw_os_error() {
Some(code) if code == libc::ESRCH => Err(CoreError::ProcessNotFound { pid }),
Some(code) if code == libc::EPERM => Err(CoreError::Permission(format!(
"permission denied sending signal {signal:?} to pid {pid}"
))),
_ => Err(CoreError::Io(err)),
}
}
pub fn renice_process(pid: u32, nice_value: i32) -> Result<(), CoreError> {
let pid_i32 = i32::try_from(pid).map_err(|_| CoreError::ProcessNotFound { pid })?;
if pid_i32 <= 0 {
return Err(CoreError::ProcessNotFound { pid });
}
let ret = unsafe {
set_errno_raw(0);
libc::setpriority(libc::PRIO_PROCESS, pid_i32 as libc::id_t, nice_value)
};
if ret == 0 {
return Ok(());
}
let err = std::io::Error::last_os_error();
match err.raw_os_error() {
Some(0) => Ok(()), Some(code) if code == libc::ESRCH => Err(CoreError::ProcessNotFound { pid }),
Some(code) if code == libc::EPERM => Err(CoreError::Permission(format!(
"permission denied changing priority of pid {pid} to {nice_value}"
))),
_ => Err(CoreError::Io(err)),
}
}
pub fn get_process_priority(pid: u32) -> Result<i32, CoreError> {
let pid_i32 = i32::try_from(pid).map_err(|_| CoreError::ProcessNotFound { pid })?;
if pid_i32 <= 0 {
return Err(CoreError::ProcessNotFound { pid });
}
let ret = unsafe {
set_errno_raw(0);
libc::getpriority(libc::PRIO_PROCESS, pid_i32 as libc::id_t)
};
let err = std::io::Error::last_os_error();
match err.raw_os_error() {
Some(0) => Ok(ret), Some(code) if code == libc::ESRCH => Err(CoreError::ProcessNotFound { pid }),
Some(code) if code == libc::EPERM => Err(CoreError::Permission(format!(
"permission denied reading priority of pid {pid}"
))),
_ => Err(CoreError::Io(err)),
}
}
unsafe fn set_errno_raw(value: i32) {
#[cfg(target_os = "macos")]
{
unsafe {
*libc::__error() = value;
}
}
#[cfg(target_os = "linux")]
{
unsafe {
*libc::__errno_location() = value;
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
let _ = value;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kill_sigterm_nonexistent_pid() {
let bad_pid: u32 = i32::MAX as u32 - 1;
let result = kill_process(bad_pid, Signal::Term);
assert!(result.is_err(), "kill(nonexistent, SIGTERM) must fail");
}
#[test]
fn test_kill_sigkill_nonexistent_pid() {
let bad_pid: u32 = i32::MAX as u32 - 1;
let result = kill_process(bad_pid, Signal::Kill);
assert!(result.is_err(), "kill(nonexistent, SIGKILL) must fail");
}
#[test]
fn test_kill_invalid_pid() {
let bad_pid: u32 = i32::MAX as u32 - 1; let result = kill_process(bad_pid, Signal::Term);
assert!(
result.is_err(),
"kill(large_pid, SIGTERM) must return an error"
);
match result.unwrap_err() {
CoreError::ProcessNotFound { .. } | CoreError::Permission(_) | CoreError::Io(_) => {}
other => panic!("unexpected error variant: {other:?}"),
}
}
#[test]
fn test_kill_u32_max_rejected() {
let result = kill_process(u32::MAX, Signal::Term);
assert!(
matches!(result, Err(CoreError::ProcessNotFound { .. })),
"u32::MAX must be rejected as ProcessNotFound, got: {result:?}"
);
}
#[test]
fn test_kill_pid_zero_rejected() {
let result = kill_process(0, Signal::Term);
assert!(
matches!(result, Err(CoreError::ProcessNotFound { .. })),
"pid 0 must be rejected, got: {result:?}"
);
}
#[test]
fn test_renice_self() {
let pid = std::process::id();
let result = renice_process(pid, 10);
assert!(
result.is_ok(),
"renice(self, 10) should succeed: {result:?}"
);
}
#[test]
fn test_renice_invalid_pid() {
let bad_pid: u32 = i32::MAX as u32 - 1;
let result = renice_process(bad_pid, 10);
assert!(
result.is_err(),
"renice(large_pid, 10) must return an error"
);
}
#[test]
fn test_kill_renice_error_types() {
let bad_pid: u32 = i32::MAX as u32 - 1;
let r = kill_process(bad_pid, Signal::Term);
if let Err(e) = r {
let is_expected = matches!(
e,
CoreError::ProcessNotFound { .. } | CoreError::Permission(_) | CoreError::Io(_)
);
assert!(is_expected, "unexpected error variant: {e:?}");
}
let r2 = renice_process(bad_pid, 0);
if let Err(e) = r2 {
let is_expected = matches!(
e,
CoreError::ProcessNotFound { .. } | CoreError::Permission(_) | CoreError::Io(_)
);
assert!(is_expected, "unexpected error variant: {e:?}");
}
}
#[test]
fn test_get_priority_self() {
let pid = std::process::id();
let result = get_process_priority(pid);
assert!(
result.is_ok(),
"get_process_priority(self) should succeed: {result:?}"
);
let nice = result.unwrap();
assert!(
(-20..=19).contains(&nice),
"nice value {nice} out of POSIX range"
);
}
#[test]
fn test_get_priority_pid_zero_rejected() {
let result = get_process_priority(0);
assert!(
matches!(result, Err(CoreError::ProcessNotFound { .. })),
"pid 0 must be rejected, got: {result:?}"
);
}
}