Skip to main content

proc_cli/core/
process.rs

1//! Cross-platform process abstraction
2//!
3//! Provides a unified interface for discovering and managing processes
4//! across macOS, Linux, and Windows.
5
6use crate::error::{ProcError, Result};
7use serde::{Deserialize, Serialize};
8use std::sync::OnceLock;
9use std::time::Duration;
10use sysinfo::{Pid, ProcessStatus as SysProcessStatus, System, Uid, Users};
11
12/// Resolve a user ID to its user name, falling back to the ID itself.
13/// The user list is read once per run.
14fn user_name(uid: &Uid) -> String {
15    static USERS: OnceLock<Users> = OnceLock::new();
16    USERS
17        .get_or_init(Users::new_with_refreshed_list)
18        .get_user_by_id(uid)
19        .map(|user| user.name().to_string())
20        .unwrap_or_else(|| uid.to_string())
21}
22
23/// On Linux, sysinfo lists every thread of a process as an entry of its own,
24/// with the process's command line, so a node server shows up a dozen times.
25/// Process listings skip them; `thread_kind` is always `None` elsewhere.
26pub fn is_thread(proc: &sysinfo::Process) -> bool {
27    proc.thread_kind().is_some()
28}
29
30/// Linux reports a working directory that was deleted as "/path (deleted)".
31/// Strip that marker so the cwd matches `--in` and reads the same as on macOS;
32/// a real directory with that name still exists, so it is kept as is.
33fn strip_deleted_marker(cwd: String) -> String {
34    match cwd.strip_suffix(" (deleted)") {
35        Some(path) if !std::path::Path::new(&cwd).exists() => path.to_string(),
36        _ => cwd,
37    }
38}
39
40/// Process status
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "lowercase")]
43pub enum ProcessStatus {
44    /// Process is actively executing on CPU
45    Running,
46    /// Process is waiting for an event or resource
47    Sleeping,
48    /// Process has been stopped (e.g., by SIGSTOP)
49    Stopped,
50    /// Process has terminated but not yet been reaped by parent
51    Zombie,
52    /// Process is being terminated
53    Dead,
54    /// Process status could not be determined
55    Unknown,
56}
57
58impl From<SysProcessStatus> for ProcessStatus {
59    fn from(status: SysProcessStatus) -> Self {
60        match status {
61            SysProcessStatus::Run => ProcessStatus::Running,
62            SysProcessStatus::Sleep => ProcessStatus::Sleeping,
63            SysProcessStatus::Stop => ProcessStatus::Stopped,
64            SysProcessStatus::Zombie => ProcessStatus::Zombie,
65            SysProcessStatus::Dead => ProcessStatus::Dead,
66            _ => ProcessStatus::Unknown,
67        }
68    }
69}
70
71/// Represents a system process with relevant information
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct Process {
74    /// Process ID
75    pub pid: u32,
76    /// Process name (executable name)
77    pub name: String,
78    /// Path to the executable
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub exe_path: Option<String>,
81    /// Current working directory
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub cwd: Option<String>,
84    /// Full command line (if available)
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub command: Option<String>,
87    /// CPU usage percentage (0.0 - 100.0+)
88    pub cpu_percent: f32,
89    /// Memory usage in megabytes
90    pub memory_mb: f64,
91    /// Process status
92    pub status: ProcessStatus,
93    /// User who owns the process
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub user: Option<String>,
96    /// Parent process ID
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub parent_pid: Option<u32>,
99    /// Process start time (Unix timestamp)
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub start_time: Option<u64>,
102    /// Seconds the process has been running
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub uptime_secs: Option<u64>,
105}
106
107impl Process {
108    /// Find all processes matching a name pattern (case-insensitive)
109    pub fn find_by_name(pattern: &str) -> Result<Vec<Process>> {
110        let mut sys = System::new_all();
111        sys.refresh_all();
112
113        let pattern_lower = pattern.to_lowercase();
114        let self_pid = sysinfo::Pid::from_u32(std::process::id());
115        // Exclude the entire ancestor chain — their command lines contain proc's
116        // arguments (e.g. "zsh -c proc by node"), causing false positives.
117        // Walk up to 10 levels (covers: proc → shell → shell wrapper → IDE → ...)
118        let ancestor_pids = {
119            let mut pids = std::collections::HashSet::new();
120            pids.insert(self_pid);
121            let mut current = self_pid;
122            for _ in 0..10 {
123                if let Some(parent) = sys.process(current).and_then(|p| p.parent()) {
124                    pids.insert(parent);
125                    current = parent;
126                } else {
127                    break;
128                }
129            }
130            pids
131        };
132        let processes: Vec<Process> = sys
133            .processes()
134            .iter()
135            .filter_map(|(pid, proc)| {
136                // Exclude self, ancestor shells, and Linux threads
137                if ancestor_pids.contains(pid) || is_thread(proc) {
138                    return None;
139                }
140
141                let name = proc.name().to_string_lossy().to_string();
142                let cmd: String = proc
143                    .cmd()
144                    .iter()
145                    .map(|s| s.to_string_lossy())
146                    .collect::<Vec<_>>()
147                    .join(" ");
148
149                // Match against name or command
150                if name.to_lowercase().contains(&pattern_lower)
151                    || cmd.to_lowercase().contains(&pattern_lower)
152                {
153                    Some(Process::from_sysinfo(*pid, proc))
154                } else {
155                    None
156                }
157            })
158            .collect();
159
160        if processes.is_empty() {
161            return Err(ProcError::ProcessNotFound(pattern.to_string()));
162        }
163
164        Ok(processes)
165    }
166
167    /// Find a specific process by PID
168    pub fn find_by_pid(pid: u32) -> Result<Option<Process>> {
169        let mut sys = System::new_all();
170        sys.refresh_all();
171
172        let sysinfo_pid = Pid::from_u32(pid);
173
174        Ok(sys
175            .processes()
176            .get(&sysinfo_pid)
177            .map(|proc| Process::from_sysinfo(sysinfo_pid, proc)))
178    }
179
180    /// Get all running processes (excludes proc's own process)
181    pub fn find_all() -> Result<Vec<Process>> {
182        let mut sys = System::new_all();
183        sys.refresh_all();
184
185        let self_pid = sysinfo::Pid::from_u32(std::process::id());
186        let processes: Vec<Process> = sys
187            .processes()
188            .iter()
189            .filter(|(pid, proc)| **pid != self_pid && !is_thread(proc))
190            .map(|(pid, proc)| Process::from_sysinfo(*pid, proc))
191            .collect();
192
193        Ok(processes)
194    }
195
196    /// Find processes running a specific executable path
197    pub fn find_by_exe_path(path: &std::path::Path) -> Result<Vec<Process>> {
198        let all = Self::find_all()?;
199        let path_str = path.to_string_lossy();
200
201        Ok(all
202            .into_iter()
203            .filter(|p| {
204                if let Some(ref exe) = p.exe_path {
205                    // Exact match (compare as strings to avoid PathBuf allocation)
206                    exe == &*path_str || std::path::Path::new(exe) == path
207                } else {
208                    false
209                }
210            })
211            .collect())
212    }
213
214    /// Find processes that have a file open (Unix only via lsof)
215    #[cfg(unix)]
216    pub fn find_by_open_file(path: &std::path::Path) -> Result<Vec<Process>> {
217        use std::process::Command;
218
219        let output = Command::new("lsof")
220            .args(["-t", &path.to_string_lossy()]) // -t = terse (PIDs only)
221            .output();
222
223        let output = match output {
224            Ok(o) => o,
225            Err(_) => return Ok(vec![]), // lsof not available
226        };
227
228        if !output.status.success() {
229            return Ok(vec![]); // No processes have file open
230        }
231
232        let pids: Vec<u32> = String::from_utf8_lossy(&output.stdout)
233            .lines()
234            .filter_map(|line| line.trim().parse().ok())
235            .collect();
236
237        let mut processes = Vec::new();
238        for pid in pids {
239            if let Ok(Some(proc)) = Self::find_by_pid(pid) {
240                processes.push(proc);
241            }
242        }
243
244        Ok(processes)
245    }
246
247    /// Find processes that have a file open (Windows stub)
248    #[cfg(not(unix))]
249    pub fn find_by_open_file(_path: &std::path::Path) -> Result<Vec<Process>> {
250        // Windows: Could use handle.exe from Sysinternals, but skip for now
251        Ok(vec![])
252    }
253
254    /// Find processes that appear to be stuck (high CPU, no progress)
255    /// This is a heuristic-based detection
256    pub fn find_stuck(timeout: Duration) -> Result<Vec<Process>> {
257        let mut sys = System::new_all();
258        sys.refresh_all();
259
260        // Wait a bit and refresh to compare
261        std::thread::sleep(Duration::from_millis(500));
262        sys.refresh_all();
263
264        let timeout_secs = timeout.as_secs();
265        let processes: Vec<Process> = sys
266            .processes()
267            .iter()
268            .filter_map(|(pid, proc)| {
269                if is_thread(proc) {
270                    return None;
271                }
272                let cpu = proc.cpu_usage();
273                let run_time = proc.run_time();
274
275                // Heuristic: Process using significant CPU for longer than timeout
276                // and in a potentially stuck state
277                if run_time > timeout_secs && cpu > 50.0 {
278                    Some(Process::from_sysinfo(*pid, proc))
279                } else {
280                    None
281                }
282            })
283            .collect();
284
285        Ok(processes)
286    }
287
288    /// Force kill the process (SIGKILL on Unix, taskkill /F on Windows)
289    pub fn kill(&self) -> Result<()> {
290        let mut sys = System::new();
291        sys.refresh_processes(
292            sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
293            true,
294        );
295
296        if let Some(proc) = sys.process(Pid::from_u32(self.pid)) {
297            if proc.kill() {
298                Ok(())
299            } else {
300                Err(ProcError::SignalError(format!(
301                    "Failed to kill process {}",
302                    self.pid
303                )))
304            }
305        } else {
306            Err(ProcError::ProcessNotFound(self.pid.to_string()))
307        }
308    }
309
310    /// Force kill and wait for process to terminate
311    /// Returns the exit status if available
312    pub fn kill_and_wait(&self) -> Result<Option<std::process::ExitStatus>> {
313        let mut sys = System::new();
314        sys.refresh_processes(
315            sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
316            true,
317        );
318
319        if let Some(proc) = sys.process(Pid::from_u32(self.pid)) {
320            proc.kill_and_wait().map_err(|e| {
321                ProcError::SignalError(format!("Failed to kill process {}: {:?}", self.pid, e))
322            })
323        } else {
324            Err(ProcError::ProcessNotFound(self.pid.to_string()))
325        }
326    }
327
328    /// Send SIGTERM for graceful termination (Unix) or taskkill (Windows)
329    #[cfg(unix)]
330    pub fn terminate(&self) -> Result<()> {
331        use nix::sys::signal::{kill, Signal};
332        use nix::unistd::Pid as NixPid;
333
334        kill(NixPid::from_raw(self.pid as i32), Signal::SIGTERM)
335            .map_err(|e| ProcError::SignalError(e.to_string()))
336    }
337
338    /// Graceful termination (Windows)
339    #[cfg(windows)]
340    pub fn terminate(&self) -> Result<()> {
341        use std::process::Command;
342
343        Command::new("taskkill")
344            .args(["/PID", &self.pid.to_string()])
345            .output()
346            .map_err(|e| ProcError::SystemError(e.to_string()))?;
347
348        Ok(())
349    }
350
351    /// Send an arbitrary signal to the process (Unix only)
352    #[cfg(unix)]
353    pub fn send_signal(&self, signal: nix::sys::signal::Signal) -> Result<()> {
354        use nix::sys::signal::kill;
355        use nix::unistd::Pid as NixPid;
356        kill(NixPid::from_raw(self.pid as i32), signal)
357            .map_err(|e| ProcError::SignalError(format!("{}: {}", signal, e)))
358    }
359
360    // Note: On Windows, send_signal is not available. Commands that use it
361    // (freeze, thaw) have their own #[cfg(not(unix))] stubs that return NotSupported.
362
363    /// Find orphaned processes (parent is PID 1 / init / launchd, excluding system daemons)
364    pub fn find_orphans() -> Result<Vec<Process>> {
365        let all = Self::find_all()?;
366
367        Ok(all
368            .into_iter()
369            .filter(|p| {
370                if let Some(ppid) = p.parent_pid {
371                    ppid == 1 && p.pid != 1 && !Self::is_system_process(p)
372                } else {
373                    false
374                }
375            })
376            .collect())
377    }
378
379    /// Check if a process is a system daemon (naturally has PPID 1)
380    ///
381    /// Filters out processes that legitimately have PPID 1 — system daemons,
382    /// kernel threads, and services managed by init/systemd/launchd.
383    /// Heuristic: no cwd or cwd is "/" with exe in system paths.
384    fn is_system_process(p: &Process) -> bool {
385        if p.cwd.is_none() || p.cwd.as_deref() == Some("/") {
386            if let Some(ref exe) = p.exe_path {
387                // macOS system paths
388                if exe.starts_with("/System/") || exe.starts_with("/usr/libexec/") {
389                    return true;
390                }
391                // Shared Unix system paths (macOS + Linux)
392                if exe.starts_with("/usr/sbin/")
393                    || exe.starts_with("/sbin/")
394                    || exe.starts_with("/usr/bin/")
395                    || exe.starts_with("/usr/lib/")
396                    || exe.starts_with("/usr/lib64/")
397                    || exe.starts_with("/lib/")
398                    || exe.starts_with("/lib64/")
399                    || exe.starts_with("/opt/")
400                    || exe.starts_with("/snap/")
401                {
402                    return true;
403                }
404            }
405            return true; // No exe path = likely kernel thread
406        }
407        false
408    }
409
410    /// Check if the process still exists
411    pub fn exists(&self) -> bool {
412        let mut sys = System::new();
413        sys.refresh_processes(
414            sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
415            true,
416        );
417        sys.process(Pid::from_u32(self.pid)).is_some()
418    }
419
420    /// Check if the process is still running (alias for exists for compatibility)
421    pub fn is_running(&self) -> bool {
422        self.exists()
423    }
424
425    /// Wait for the process to terminate
426    /// Returns the exit status if available
427    pub fn wait(&self) -> Option<std::process::ExitStatus> {
428        let mut sys = System::new();
429        sys.refresh_processes(
430            sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
431            true,
432        );
433
434        sys.process(Pid::from_u32(self.pid))
435            .and_then(|proc| proc.wait())
436    }
437
438    /// Convert from sysinfo Process
439    pub(crate) fn from_sysinfo(pid: Pid, proc: &sysinfo::Process) -> Self {
440        let cmd_vec = proc.cmd();
441        let command = if cmd_vec.is_empty() {
442            None
443        } else {
444            Some(
445                cmd_vec
446                    .iter()
447                    .map(|s| s.to_string_lossy())
448                    .collect::<Vec<_>>()
449                    .join(" "),
450            )
451        };
452
453        let exe_path = proc.exe().map(|p| p.to_string_lossy().to_string());
454        let cwd = proc
455            .cwd()
456            .map(|p| strip_deleted_marker(p.to_string_lossy().to_string()));
457
458        Process {
459            pid: pid.as_u32(),
460            name: proc.name().to_string_lossy().to_string(),
461            exe_path,
462            cwd,
463            command,
464            cpu_percent: proc.cpu_usage(),
465            memory_mb: proc.memory() as f64 / 1024.0 / 1024.0,
466            status: ProcessStatus::from(proc.status()),
467            user: proc.user_id().map(user_name),
468            parent_pid: proc.parent().map(|p| p.as_u32()),
469            // sysinfo reports 0 when it can't read the start time (on macOS, any
470            // other user's process), which would make the uptime about 56 years
471            start_time: (proc.start_time() > 0).then(|| proc.start_time()),
472            uptime_secs: (proc.start_time() > 0).then(|| proc.run_time()),
473        }
474    }
475}
476
477/// Parse a signal name to a nix Signal (Unix only)
478///
479/// Accepts: "HUP", "SIGHUP", "hup" (signal names only, not numbers —
480/// numeric signal values differ between macOS and Linux)
481#[cfg(unix)]
482pub fn parse_signal_name(name: &str) -> Result<nix::sys::signal::Signal> {
483    use nix::sys::signal::Signal;
484
485    let upper = name.to_uppercase();
486    let upper = upper.trim_start_matches("SIG");
487    match upper {
488        "HUP" => Ok(Signal::SIGHUP),
489        "INT" => Ok(Signal::SIGINT),
490        "QUIT" => Ok(Signal::SIGQUIT),
491        "ABRT" => Ok(Signal::SIGABRT),
492        "KILL" => Ok(Signal::SIGKILL),
493        "TERM" => Ok(Signal::SIGTERM),
494        "STOP" => Ok(Signal::SIGSTOP),
495        "CONT" => Ok(Signal::SIGCONT),
496        "USR1" => Ok(Signal::SIGUSR1),
497        "USR2" => Ok(Signal::SIGUSR2),
498        _ => Err(ProcError::InvalidInput(format!(
499            "Unknown signal: '{}'. Valid signals: HUP, INT, QUIT, ABRT, KILL, TERM, STOP, CONT, USR1, USR2",
500            name
501        ))),
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508
509    #[test]
510    fn test_find_all_processes() {
511        let processes = Process::find_all().unwrap();
512        assert!(!processes.is_empty(), "Should find at least one process");
513    }
514
515    #[test]
516    fn test_find_by_pid_self() {
517        let pid = std::process::id();
518        let process = Process::find_by_pid(pid).unwrap();
519        assert!(process.is_some(), "Should find own process");
520    }
521
522    #[test]
523    fn test_strip_deleted_marker() {
524        assert_eq!(
525            strip_deleted_marker("/nonexistent/proc-gone (deleted)".to_string()),
526            "/nonexistent/proc-gone"
527        );
528        assert_eq!(
529            strip_deleted_marker("/tmp/project".to_string()),
530            "/tmp/project"
531        );
532
533        // A directory really named "x (deleted)" keeps its name
534        let real = std::env::temp_dir().join(format!("proc-real-{} (deleted)", std::process::id()));
535        std::fs::create_dir_all(&real).unwrap();
536        let path = real.to_string_lossy().to_string();
537        assert_eq!(strip_deleted_marker(path.clone()), path);
538        std::fs::remove_dir(&real).unwrap();
539    }
540
541    #[test]
542    fn test_find_nonexistent_process() {
543        let result = Process::find_by_name("nonexistent_process_12345");
544        assert!(result.is_err());
545    }
546
547    #[test]
548    fn test_find_orphans_returns_ok() {
549        // Should not error — may or may not find orphans depending on system state
550        let result = Process::find_orphans();
551        assert!(result.is_ok());
552    }
553
554    #[test]
555    fn test_find_orphans_excludes_system_processes() {
556        let orphans = Process::find_orphans().unwrap();
557        for orphan in &orphans {
558            // No orphan should have a system exe path with cwd "/"
559            if orphan.cwd.as_deref() == Some("/") {
560                if let Some(ref exe) = orphan.exe_path {
561                    assert!(
562                        !exe.starts_with("/usr/sbin/")
563                            && !exe.starts_with("/sbin/")
564                            && !exe.starts_with("/System/")
565                            && !exe.starts_with("/usr/libexec/"),
566                        "System process should have been filtered: {} ({})",
567                        orphan.name,
568                        exe
569                    );
570                }
571            }
572        }
573    }
574
575    #[test]
576    fn test_is_system_process_system_paths() {
577        let make_proc = |exe: Option<&str>, cwd: Option<&str>| Process {
578            pid: 100,
579            name: "test".to_string(),
580            exe_path: exe.map(String::from),
581            cwd: cwd.map(String::from),
582            command: None,
583            cpu_percent: 0.0,
584            memory_mb: 0.0,
585            status: ProcessStatus::Running,
586            user: None,
587            parent_pid: Some(1),
588            start_time: None,
589            uptime_secs: None,
590        };
591
592        // System daemons with cwd "/" and system exe paths
593        assert!(Process::is_system_process(&make_proc(
594            Some("/usr/sbin/sshd"),
595            Some("/")
596        )));
597        assert!(Process::is_system_process(&make_proc(
598            Some("/System/Library/foo"),
599            Some("/")
600        )));
601        assert!(Process::is_system_process(&make_proc(
602            Some("/usr/bin/systemd"),
603            Some("/")
604        )));
605        assert!(Process::is_system_process(&make_proc(
606            Some("/usr/lib/snapd/snapd"),
607            Some("/")
608        )));
609
610        // No exe path with cwd "/" = kernel thread
611        assert!(Process::is_system_process(&make_proc(None, Some("/"))));
612
613        // No cwd at all = likely kernel thread
614        assert!(Process::is_system_process(&make_proc(
615            Some("/usr/bin/foo"),
616            None
617        )));
618
619        // User process with user cwd = NOT system
620        assert!(!Process::is_system_process(&make_proc(
621            Some("/usr/bin/node"),
622            Some("/home/user/project")
623        )));
624        assert!(!Process::is_system_process(&make_proc(
625            Some("/home/user/.local/bin/app"),
626            Some("/home/user")
627        )));
628    }
629
630    #[cfg(unix)]
631    #[test]
632    fn test_parse_signal_name_valid() {
633        use nix::sys::signal::Signal;
634
635        assert_eq!(parse_signal_name("HUP").unwrap(), Signal::SIGHUP);
636        assert_eq!(parse_signal_name("hup").unwrap(), Signal::SIGHUP);
637        assert_eq!(parse_signal_name("SIGHUP").unwrap(), Signal::SIGHUP);
638        assert_eq!(parse_signal_name("sighup").unwrap(), Signal::SIGHUP);
639        assert_eq!(parse_signal_name("INT").unwrap(), Signal::SIGINT);
640        assert_eq!(parse_signal_name("QUIT").unwrap(), Signal::SIGQUIT);
641        assert_eq!(parse_signal_name("ABRT").unwrap(), Signal::SIGABRT);
642        assert_eq!(parse_signal_name("KILL").unwrap(), Signal::SIGKILL);
643        assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
644        assert_eq!(parse_signal_name("STOP").unwrap(), Signal::SIGSTOP);
645        assert_eq!(parse_signal_name("CONT").unwrap(), Signal::SIGCONT);
646        assert_eq!(parse_signal_name("USR1").unwrap(), Signal::SIGUSR1);
647        assert_eq!(parse_signal_name("USR2").unwrap(), Signal::SIGUSR2);
648    }
649
650    #[cfg(unix)]
651    #[test]
652    fn test_parse_signal_name_invalid() {
653        assert!(parse_signal_name("INVALID").is_err());
654        assert!(parse_signal_name("FOO").is_err());
655        assert!(parse_signal_name("").is_err());
656    }
657
658    #[cfg(unix)]
659    #[test]
660    fn test_parse_signal_name_case_insensitive() {
661        use nix::sys::signal::Signal;
662
663        assert_eq!(parse_signal_name("term").unwrap(), Signal::SIGTERM);
664        assert_eq!(parse_signal_name("Term").unwrap(), Signal::SIGTERM);
665        assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
666        assert_eq!(parse_signal_name("sigterm").unwrap(), Signal::SIGTERM);
667        assert_eq!(parse_signal_name("SigTerm").unwrap(), Signal::SIGTERM);
668    }
669
670    #[cfg(unix)]
671    #[test]
672    fn test_send_signal_nonexistent_process() {
673        use nix::sys::signal::Signal;
674
675        // PID 99999999 almost certainly doesn't exist
676        let proc = Process {
677            pid: 99999999,
678            name: "ghost".to_string(),
679            exe_path: None,
680            cwd: None,
681            command: None,
682            cpu_percent: 0.0,
683            memory_mb: 0.0,
684            status: ProcessStatus::Running,
685            user: None,
686            parent_pid: None,
687            start_time: None,
688            uptime_secs: None,
689        };
690
691        let result = proc.send_signal(Signal::SIGCONT);
692        assert!(result.is_err());
693    }
694}