use serde::{Deserialize, Serialize};
use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System};
use tracing::{debug, info};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessInfo {
pub pid: u32,
pub name: String,
pub parent_pid: Option<u32>,
pub cpu_usage: f32,
pub memory: u64,
pub virtual_memory: u64,
pub status: String,
pub start_time: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessFilter {
pub name: Option<String>,
pub exact_match: bool,
pub min_cpu: Option<f32>,
pub min_memory: Option<u64>,
pub status: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ProcessSortBy {
Pid,
Name,
Cpu,
Memory,
Status,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ProcessStatus {
Running,
Sleeping,
Stopped,
Zombie,
Unknown,
}
pub fn get_all_processes() -> Vec<ProcessInfo> {
debug!("Getting all running processes");
let mut system = System::new();
system.refresh_processes_specifics(sysinfo::ProcessesToUpdate::All, true, ProcessRefreshKind::everything());
let mut processes = Vec::new();
for (pid, process) in system.processes() {
processes.push(ProcessInfo {
pid: pid.as_u32(),
name: process.name().to_string_lossy().to_string(),
parent_pid: process.parent().map(|p| p.as_u32()),
cpu_usage: process.cpu_usage(),
memory: process.memory(),
virtual_memory: process.virtual_memory(),
status: format!("{:?}", process.status()),
start_time: Some(process.start_time()),
});
}
info!("Retrieved {} running processes", processes.len());
processes
}
pub fn get_processes_by_filter(filter: &ProcessFilter) -> Vec<ProcessInfo> {
debug!("Getting processes by filter: {:?}", filter);
let all = get_all_processes();
let filtered: Vec<ProcessInfo> = all
.into_iter()
.filter(|p| {
if let Some(name) = &filter.name {
if filter.exact_match {
if !p.name.eq_ignore_ascii_case(name) {
return false;
}
} else if !p.name.to_lowercase().contains(&name.to_lowercase()) {
return false;
}
}
if let Some(min_cpu) = filter.min_cpu {
if p.cpu_usage < min_cpu {
return false;
}
}
if let Some(min_memory) = filter.min_memory {
if p.memory < min_memory {
return false;
}
}
if let Some(status) = &filter.status {
if !p.status.to_lowercase().contains(&status.to_lowercase()) {
return false;
}
}
true
})
.collect();
info!("Filtered to {} processes matching criteria", filtered.len());
filtered
}
pub fn get_process_by_pid(pid: u32) -> Option<ProcessInfo> {
debug!("Getting process by PID: {}", pid);
let mut system = System::new();
system.refresh_processes_specifics(sysinfo::ProcessesToUpdate::All, true, ProcessRefreshKind::everything());
let pid_obj = Pid::from_u32(pid);
let result = system.process(pid_obj).map(|process| ProcessInfo {
pid: pid,
name: process.name().to_string_lossy().to_string(),
parent_pid: process.parent().map(|p| p.as_u32()),
cpu_usage: process.cpu_usage(),
memory: process.memory(),
virtual_memory: process.virtual_memory(),
status: format!("{:?}", process.status()),
start_time: Some(process.start_time()),
});
if result.is_some() {
info!("Found process with PID: {}", pid);
} else {
debug!("Process with PID {} not found", pid);
}
result
}
pub fn get_pids_by_name(name: &str, exact_match: bool) -> Vec<u32> {
debug!("Getting PIDs by name: '{}', exact_match: {}", name, exact_match);
let system = System::new();
let name_lower = name.to_lowercase();
let pids: Vec<u32> = system
.processes()
.iter()
.filter(|(_, p)| {
let proc_name = p.name().to_string_lossy().to_lowercase();
if exact_match { proc_name == name_lower } else { proc_name.contains(&name_lower) }
})
.map(|(pid, _)| pid.as_u32())
.collect();
info!("Found {} PIDs matching '{}'", pids.len(), name);
pids
}
pub fn is_process_running(name: &str, exact_match: bool) -> bool {
debug!("Checking if process '{}' is running (exact: {})", name, exact_match);
let running = !get_pids_by_name(name, exact_match).is_empty();
info!("Process '{}' running: {}", name, running);
running
}
pub fn kill_process(pid: u32, force: bool) -> Result<(), String> {
debug!("Terminating process PID: {}, force: {}", pid, force);
#[cfg(target_os = "windows")]
{
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_TERMINATE, TerminateProcess};
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
if handle == std::ptr::null_mut() {
debug!("Failed to open process with PID: {}", pid);
return Err(format!("Failed to open process with PID: {}", pid));
}
let result = TerminateProcess(handle, 1);
CloseHandle(handle);
if result == 0 {
debug!("Failed to terminate process with PID: {}", pid);
return Err(format!("Failed to terminate process with PID: {}", pid));
}
}
info!("Process {} terminated successfully on Windows", pid);
Ok(())
}
#[cfg(not(target_os = "windows"))]
{
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid as NixPid;
let signal = if force { Signal::SIGKILL } else { Signal::SIGTERM };
let result = kill(NixPid::from_raw(pid as i32), signal).map_err(|e| format!("Failed to kill process {}: {}", pid, e));
if result.is_ok() {
info!("Process {} terminated successfully on Unix", pid);
} else {
debug!("Failed to terminate process {}: {:?}", pid, result);
}
result
}
}
pub fn kill_processes_by_name(name: &str, force: bool) -> Result<usize, String> {
debug!("Terminating all processes by name: '{}', force: {}", name, force);
let pids = get_pids_by_name(name, false);
if pids.is_empty() {
info!("No processes found matching '{}'", name);
return Ok(0);
}
let mut killed = 0;
for pid in pids {
if kill_process(pid, force).is_ok() {
killed += 1;
}
}
info!("Terminated {} process(es) matching '{}'", killed, name);
Ok(killed)
}
pub fn sort_processes(processes: &mut [ProcessInfo], sort_by: ProcessSortBy) {
debug!("Sorting processes by: {:?}", sort_by);
match sort_by {
ProcessSortBy::Pid => processes.sort_by_key(|p| p.pid),
ProcessSortBy::Name => processes.sort_by(|a, b| a.name.cmp(&b.name)),
ProcessSortBy::Cpu => processes.sort_by(|a, b| b.cpu_usage.partial_cmp(&a.cpu_usage).unwrap_or(std::cmp::Ordering::Equal)),
ProcessSortBy::Memory => processes.sort_by_key(|p| std::cmp::Reverse(p.memory)),
ProcessSortBy::Status => processes.sort_by(|a, b| a.status.cmp(&b.status)),
}
info!("Sorted {} processes", processes.len());
}
pub fn format_memory(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
pub fn process_status_to_string(status: &str) -> &'static str {
match status {
"Running" => "Running",
"Sleeping" => "Sleeping",
"Stopped" => "Stopped",
"Zombie" => "Zombie",
_ => "Unknown",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_all_processes() {
let processes = get_all_processes();
assert!(!processes.is_empty());
let has_valid = processes.iter().any(|p| p.pid > 0);
assert!(has_valid);
}
#[test]
fn test_get_process_by_pid() {
let processes = get_all_processes();
if let Some(first) = processes.first() {
let process = get_process_by_pid(first.pid);
assert!(process.is_some());
assert_eq!(process.unwrap().pid, first.pid);
}
}
#[test]
fn test_is_process_running() {
let result = is_process_running("system", false);
assert!(result == true || result == false);
}
#[test]
fn test_format_memory() {
assert_eq!(format_memory(0), "0 B");
assert_eq!(format_memory(1024), "1.00 KB");
assert_eq!(format_memory(1024 * 1024), "1.00 MB");
assert_eq!(format_memory(1024 * 1024 * 1024), "1.00 GB");
}
#[test]
fn test_process_status_to_string() {
assert_eq!(process_status_to_string("Running"), "Running");
assert_eq!(process_status_to_string("Sleeping"), "Sleeping");
assert_eq!(process_status_to_string("Stopped"), "Stopped");
assert_eq!(process_status_to_string("Zombie"), "Zombie");
assert_eq!(process_status_to_string("Unknown"), "Unknown");
}
}