1use crate::error::{ProcError, Result};
7use serde::{Deserialize, Serialize};
8use std::time::Duration;
9use sysinfo::{Pid, ProcessStatus as SysProcessStatus, System};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum ProcessStatus {
15 Running,
17 Sleeping,
19 Stopped,
21 Zombie,
23 Dead,
25 Unknown,
27}
28
29impl From<SysProcessStatus> for ProcessStatus {
30 fn from(status: SysProcessStatus) -> Self {
31 match status {
32 SysProcessStatus::Run => ProcessStatus::Running,
33 SysProcessStatus::Sleep => ProcessStatus::Sleeping,
34 SysProcessStatus::Stop => ProcessStatus::Stopped,
35 SysProcessStatus::Zombie => ProcessStatus::Zombie,
36 SysProcessStatus::Dead => ProcessStatus::Dead,
37 _ => ProcessStatus::Unknown,
38 }
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Process {
45 pub pid: u32,
47 pub name: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub exe_path: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub cwd: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub command: Option<String>,
58 pub cpu_percent: f32,
60 pub memory_mb: f64,
62 pub status: ProcessStatus,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub user: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub parent_pid: Option<u32>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub start_time: Option<u64>,
73}
74
75impl Process {
76 pub fn find_by_name(pattern: &str) -> Result<Vec<Process>> {
78 let mut sys = System::new_all();
79 sys.refresh_all();
80
81 let pattern_lower = pattern.to_lowercase();
82 let processes: Vec<Process> = sys
83 .processes()
84 .iter()
85 .filter_map(|(pid, proc)| {
86 let name = proc.name().to_string_lossy().to_string();
87 let cmd: String = proc
88 .cmd()
89 .iter()
90 .map(|s| s.to_string_lossy())
91 .collect::<Vec<_>>()
92 .join(" ");
93
94 if name.to_lowercase().contains(&pattern_lower)
96 || cmd.to_lowercase().contains(&pattern_lower)
97 {
98 Some(Process::from_sysinfo(*pid, proc))
99 } else {
100 None
101 }
102 })
103 .collect();
104
105 if processes.is_empty() {
106 return Err(ProcError::ProcessNotFound(pattern.to_string()));
107 }
108
109 Ok(processes)
110 }
111
112 pub fn find_by_pid(pid: u32) -> Result<Option<Process>> {
114 let mut sys = System::new_all();
115 sys.refresh_all();
116
117 let sysinfo_pid = Pid::from_u32(pid);
118
119 Ok(sys
120 .processes()
121 .get(&sysinfo_pid)
122 .map(|proc| Process::from_sysinfo(sysinfo_pid, proc)))
123 }
124
125 pub fn find_all() -> Result<Vec<Process>> {
127 let mut sys = System::new_all();
128 sys.refresh_all();
129
130 let processes: Vec<Process> = sys
131 .processes()
132 .iter()
133 .map(|(pid, proc)| Process::from_sysinfo(*pid, proc))
134 .collect();
135
136 Ok(processes)
137 }
138
139 pub fn find_by_exe_path(path: &std::path::Path) -> Result<Vec<Process>> {
141 let all = Self::find_all()?;
142 let path_str = path.to_string_lossy();
143
144 Ok(all
145 .into_iter()
146 .filter(|p| {
147 if let Some(ref exe) = p.exe_path {
148 exe == &*path_str || std::path::Path::new(exe) == path
150 } else {
151 false
152 }
153 })
154 .collect())
155 }
156
157 #[cfg(unix)]
159 pub fn find_by_open_file(path: &std::path::Path) -> Result<Vec<Process>> {
160 use std::process::Command;
161
162 let output = Command::new("lsof")
163 .args(["-t", &path.to_string_lossy()]) .output();
165
166 let output = match output {
167 Ok(o) => o,
168 Err(_) => return Ok(vec![]), };
170
171 if !output.status.success() {
172 return Ok(vec![]); }
174
175 let pids: Vec<u32> = String::from_utf8_lossy(&output.stdout)
176 .lines()
177 .filter_map(|line| line.trim().parse().ok())
178 .collect();
179
180 let mut processes = Vec::new();
181 for pid in pids {
182 if let Ok(Some(proc)) = Self::find_by_pid(pid) {
183 processes.push(proc);
184 }
185 }
186
187 Ok(processes)
188 }
189
190 #[cfg(not(unix))]
192 pub fn find_by_open_file(_path: &std::path::Path) -> Result<Vec<Process>> {
193 Ok(vec![])
195 }
196
197 pub fn find_stuck(timeout: Duration) -> Result<Vec<Process>> {
200 let mut sys = System::new_all();
201 sys.refresh_all();
202
203 std::thread::sleep(Duration::from_millis(500));
205 sys.refresh_all();
206
207 let timeout_secs = timeout.as_secs();
208 let processes: Vec<Process> = sys
209 .processes()
210 .iter()
211 .filter_map(|(pid, proc)| {
212 let cpu = proc.cpu_usage();
213 let run_time = proc.run_time();
214
215 if run_time > timeout_secs && cpu > 50.0 {
218 Some(Process::from_sysinfo(*pid, proc))
219 } else {
220 None
221 }
222 })
223 .collect();
224
225 Ok(processes)
226 }
227
228 pub fn kill(&self) -> Result<()> {
230 let mut sys = System::new();
231 sys.refresh_processes(
232 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
233 true,
234 );
235
236 if let Some(proc) = sys.process(Pid::from_u32(self.pid)) {
237 if proc.kill() {
238 Ok(())
239 } else {
240 Err(ProcError::SignalError(format!(
241 "Failed to kill process {}",
242 self.pid
243 )))
244 }
245 } else {
246 Err(ProcError::ProcessNotFound(self.pid.to_string()))
247 }
248 }
249
250 pub fn kill_and_wait(&self) -> Result<Option<std::process::ExitStatus>> {
253 let mut sys = System::new();
254 sys.refresh_processes(
255 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
256 true,
257 );
258
259 if let Some(proc) = sys.process(Pid::from_u32(self.pid)) {
260 proc.kill_and_wait().map_err(|e| {
261 ProcError::SignalError(format!("Failed to kill process {}: {:?}", self.pid, e))
262 })
263 } else {
264 Err(ProcError::ProcessNotFound(self.pid.to_string()))
265 }
266 }
267
268 #[cfg(unix)]
270 pub fn terminate(&self) -> Result<()> {
271 use nix::sys::signal::{kill, Signal};
272 use nix::unistd::Pid as NixPid;
273
274 kill(NixPid::from_raw(self.pid as i32), Signal::SIGTERM)
275 .map_err(|e| ProcError::SignalError(e.to_string()))
276 }
277
278 #[cfg(windows)]
280 pub fn terminate(&self) -> Result<()> {
281 use std::process::Command;
282
283 Command::new("taskkill")
284 .args(["/PID", &self.pid.to_string()])
285 .output()
286 .map_err(|e| ProcError::SystemError(e.to_string()))?;
287
288 Ok(())
289 }
290
291 pub fn exists(&self) -> bool {
293 let mut sys = System::new();
294 sys.refresh_processes(
295 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
296 true,
297 );
298 sys.process(Pid::from_u32(self.pid)).is_some()
299 }
300
301 pub fn is_running(&self) -> bool {
303 self.exists()
304 }
305
306 pub fn wait(&self) -> Option<std::process::ExitStatus> {
309 let mut sys = System::new();
310 sys.refresh_processes(
311 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
312 true,
313 );
314
315 sys.process(Pid::from_u32(self.pid))
316 .and_then(|proc| proc.wait())
317 }
318
319 fn from_sysinfo(pid: Pid, proc: &sysinfo::Process) -> Self {
321 let cmd_vec = proc.cmd();
322 let command = if cmd_vec.is_empty() {
323 None
324 } else {
325 Some(
326 cmd_vec
327 .iter()
328 .map(|s| s.to_string_lossy())
329 .collect::<Vec<_>>()
330 .join(" "),
331 )
332 };
333
334 let exe_path = proc.exe().map(|p| p.to_string_lossy().to_string());
335 let cwd = proc.cwd().map(|p| p.to_string_lossy().to_string());
336
337 Process {
338 pid: pid.as_u32(),
339 name: proc.name().to_string_lossy().to_string(),
340 exe_path,
341 cwd,
342 command,
343 cpu_percent: proc.cpu_usage(),
344 memory_mb: proc.memory() as f64 / 1024.0 / 1024.0,
345 status: ProcessStatus::from(proc.status()),
346 user: proc.user_id().map(|u| u.to_string()),
347 parent_pid: proc.parent().map(|p| p.as_u32()),
348 start_time: Some(proc.start_time()),
349 }
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn test_find_all_processes() {
359 let processes = Process::find_all().unwrap();
360 assert!(!processes.is_empty(), "Should find at least one process");
361 }
362
363 #[test]
364 fn test_find_by_pid_self() {
365 let pid = std::process::id();
366 let process = Process::find_by_pid(pid).unwrap();
367 assert!(process.is_some(), "Should find own process");
368 }
369
370 #[test]
371 fn test_find_nonexistent_process() {
372 let result = Process::find_by_name("nonexistent_process_12345");
373 assert!(result.is_err());
374 }
375}